summaryrefslogtreecommitdiff
path: root/clams-server/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'clams-server/src/main/java')
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java81
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppConfig.java2
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java47
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java38
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java1
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/MediaController.java157
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/PostController.java31
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/ImageMetaData.java11
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/Post.java8
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostDTO.java4
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java5
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java92
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepo.java13
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepoSqlite.java120
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/SqliteManager.java10
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/SlugService.java42
16 files changed, 629 insertions, 33 deletions
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
index 18dd2cd..8f494ed 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
@@ -1,40 +1,61 @@
package no.eliashaugsbakk.clams.server;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import io.javalin.Javalin;
import io.javalin.rendering.template.JavalinPebble;
-import java.nio.file.Path;
import java.util.Map;
-import no.eliashaugsbakk.clams.server.config.AppConfig;
-import no.eliashaugsbakk.clams.server.controller.BlogController;
-import no.eliashaugsbakk.clams.server.repository.SqliteManager;
+import no.eliashaugsbakk.clams.server.config.AppContext;
+import no.eliashaugsbakk.clams.server.config.AppRoutes;
public class Main {
void main() {
- AppConfig appConfig = new AppConfig();
- appConfig.loadConfig();
- String dbUrl = Path.of(appConfig.getStorageLocation()).resolve("posts.db").toString();
- SqliteManager dbManager = new SqliteManager(dbUrl);
- dbManager.init();
-
- BlogController blogController = new BlogController(dbManager);
-
- var app = Javalin.create(config -> {
- config.staticFiles.add("/public");
- config.fileRenderer(new JavalinPebble());
-
- config.routes.error(404, ctx -> ctx.render("templates/404.html",
- Map.of("page_title", "404 - Page Not Found", "page_css", "404")));
-
- config.routes.get("/", ctx -> ctx.redirect("/home"));
- config.routes.get("/home", ctx -> ctx.render("templates/home.html",
- Map.of("page_title", "Elias Haugsbakk", "page_css", "home")));
- config.routes.get("/projects", ctx -> ctx.render("templates/projects.html",
- Map.of("page_title", "Projects - Elias Haugsbakk", "page_css", "projects")));
-
- config.routes.get("/blog", blogController::handleBlogRequest);
- config.routes.get("/blog/{slug}", blogController::handleGetPost);
-
- config.events.serverStopped(dbManager::close);
- }).start(7070);
+ // Initialize dependencies
+ AppContext context = new AppContext();
+
+ try {
+ Javalin.create(config -> {
+ config.staticFiles.add("/public");
+ config.fileRenderer(new JavalinPebble());
+
+ config.routes.error(404, ctx -> {
+ if (ctx.path().startsWith("/api")) {
+ ctx.json(Map.of(
+ "error", "Not Found",
+ "message", "The requested API endpoint does not exist."
+ ));
+ } else {
+ ctx.render("templates/404.html", Map.of(
+ "page_title", "404 - Page Not Found",
+ "page_css", "404"
+ ));
+ }
+ });
+
+ config.routes.exception(UnrecognizedPropertyException.class, (e, ctx) -> ctx.status(400).json(Map.of(
+ "error", "Bad Request",
+ "message", "Unrecognized property: '" + e.getPropertyName() + "'"
+ )));
+
+ config.routes.exception(Exception.class, (e, ctx) -> {
+ e.printStackTrace();
+ ctx.status(500);
+
+ if (ctx.path().startsWith("/api")) {
+ ctx.json(Map.of("error", "Internal Server Error"));
+ } else {
+ ctx.result("Internal Server Error. Please try again later.");
+ }
+ });
+
+ config.routes.apiBuilder(new AppRoutes(context));
+
+ config.events.serverStopped(context::close);
+ }).start(7070);
+
+
+ } catch (Exception e) {
+ context.close();
+ throw e;
+ }
}
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppConfig.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppConfig.java
index 8f460f5..1d68166 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppConfig.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppConfig.java
@@ -54,6 +54,6 @@ public class AppConfig {
}
public String getStorageLocation() {
- return properties.getProperty("storage_location", "./data/");
+ return properties.getProperty("storage_location", "./data");
}
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java
new file mode 100644
index 0000000..c5ef242
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppContext.java
@@ -0,0 +1,47 @@
+package no.eliashaugsbakk.clams.server.config;
+
+import java.nio.file.Path;
+import no.eliashaugsbakk.clams.server.controller.BlogController;
+import no.eliashaugsbakk.clams.server.controller.MediaController;
+import no.eliashaugsbakk.clams.server.controller.PostController;
+import no.eliashaugsbakk.clams.server.repository.BlogPostRepo;
+import no.eliashaugsbakk.clams.server.repository.BlogPostRepoSqlite;
+import no.eliashaugsbakk.clams.server.repository.MediaRepo;
+import no.eliashaugsbakk.clams.server.repository.MediaRepoSqlite;
+import no.eliashaugsbakk.clams.server.repository.SqliteManager;
+import no.eliashaugsbakk.clams.server.service.SlugService;
+
+public class AppContext implements AutoCloseable {
+ private final SqliteManager dbManager;
+ private final BlogController blogController;
+ private final MediaController mediaController;
+ private final PostController postController;
+
+ public AppContext() {
+ AppConfig appConfig = new AppConfig();
+ appConfig.loadConfig();
+ String dbUrl = Path.of(appConfig.getStorageLocation()).resolve("clams.db").toString();
+
+ this.dbManager = new SqliteManager(dbUrl);
+ this.dbManager.init();
+
+ BlogPostRepo blogPostRepo = new BlogPostRepoSqlite(dbManager);
+ MediaRepo mediaRepo = new MediaRepoSqlite(dbManager);
+ SlugService slugService = new SlugService(blogPostRepo);
+
+ this.blogController = new BlogController(dbManager);
+ this.mediaController = new MediaController(mediaRepo, appConfig);
+ this.postController = new PostController(blogPostRepo, slugService);
+ }
+
+ public BlogController getBlogController() { return blogController; }
+ public MediaController getMediaController() { return mediaController; }
+ public PostController getPostController() { return postController; }
+
+ @Override
+ public void close() {
+ if (dbManager != null) {
+ dbManager.close();
+ }
+ }
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java
new file mode 100644
index 0000000..fb42dfc
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/config/AppRoutes.java
@@ -0,0 +1,38 @@
+package no.eliashaugsbakk.clams.server.config;
+
+import static io.javalin.apibuilder.ApiBuilder.*;
+import io.javalin.apibuilder.EndpointGroup;
+import java.util.Map;
+
+public class AppRoutes implements EndpointGroup {
+ private final AppContext ctx;
+
+ public AppRoutes(AppContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void addEndpoints() {
+ get("/", ctx -> ctx.redirect("/home"));
+ get("/home", ctx -> ctx.render("templates/home.html",
+ Map.of("page_title", "Elias Haugsbakk", "page_css", "home")));
+ get("/projects", ctx -> ctx.render("templates/projects.html",
+ Map.of("page_title", "Projects - Elias Haugsbakk", "page_css", "projects")));
+
+ path("blog", () -> {
+ get(ctx.getBlogController()::handleBlogRequest);
+ get("{slug}", ctx.getBlogController()::handleGetPost);
+ });
+
+ path("api", () -> {
+ post("posts", ctx.getPostController()::handlePostPost);
+ put("posts/{slug}", ctx.getPostController()::handlePutPost);
+ delete("posts/{slug}", ctx.getPostController()::handleDeletePost);
+
+ get("media", ctx.getMediaController()::handleGetMediaIndex);
+ get("media/{uuid}", ctx.getMediaController()::handleGetMedia);
+ post("media", ctx.getMediaController()::handlePostMedia);
+ delete("media/{uuid}", ctx.getMediaController()::handleDeleteMedia);
+ });
+ }
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
index 58ce6d4..33a6ac6 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
@@ -74,7 +74,6 @@ public class BlogController {
.toList();
Map<Integer, List<PostMetaData>> postsByYear = groupPostsByYear(allPosts);
- // Manually list featured posts
var featuredSlugs = List.of(
""
);
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/MediaController.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/MediaController.java
new file mode 100644
index 0000000..8855b7b
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/MediaController.java
@@ -0,0 +1,157 @@
+package no.eliashaugsbakk.clams.server.controller;
+
+import io.javalin.http.Context;
+import io.javalin.http.UploadedFile;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import no.eliashaugsbakk.clams.server.config.AppConfig;
+import no.eliashaugsbakk.clams.server.model.ImageMetaData;
+import no.eliashaugsbakk.clams.server.repository.MediaRepo;
+import org.apache.commons.imaging.ImageFormat;
+import org.apache.commons.imaging.ImageFormats;
+import org.apache.commons.imaging.ImageInfo;
+import org.apache.commons.imaging.Imaging;
+
+public class MediaController {
+ private final MediaRepo mediaRepo;
+ private final AppConfig appConfig;
+
+ public MediaController(MediaRepo mediaRepo, AppConfig appConfig) {
+ this.mediaRepo = mediaRepo;
+ this.appConfig = appConfig;
+ }
+
+ public void handlePostMedia(Context ctx) {
+ UploadedFile file = ctx.uploadedFile("image");
+
+ if (file == null) {
+ ctx.status(400).result("Missing image file payload.");
+ return;
+ }
+
+ try (InputStream is = file.content()) {
+ byte[] imageBytes = is.readAllBytes();
+
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes)) {
+ ImageInfo info = Imaging.getImageInfo(bais, file.filename());
+ ImageFormat format = info.getFormat();
+
+ if (format != ImageFormats.JPEG) {
+ ctx.status(415).result("Unsupported format: only jpeg allowed.");
+ return;
+ }
+
+ if (info.getWidth() > 2000 || info.getHeight() > 2000) {
+ ctx.status(400).result("Image dimensions are too large: max allowed 2000x2000");
+ return;
+ }
+ }
+
+ UUID generatedUuid = saveToStorage(imageBytes, file.filename(), file.contentType());
+
+ ctx.status(201).json(Map.of("uuid", generatedUuid, "url", "/api/media/" + generatedUuid));
+ } catch (Exception e) {
+ ctx.status(400).result("Corrupted or invalid image data.");
+ }
+ }
+
+ private UUID saveToStorage(byte[] bytes, String originalFilename, String contentType)
+ throws IOException {
+ UUID uuid = UUID.randomUUID();
+ Path path = Path.of(appConfig.getStorageLocation(), "images", uuid + ".jpeg");
+ Files.createDirectories(path.getParent());
+ Files.write(path, bytes);
+
+ try {
+ mediaRepo.addImage(new ImageMetaData(uuid, originalFilename, contentType, Instant.now()));
+ return uuid;
+ } catch (Exception e) {
+ Files.deleteIfExists(path);
+ throw new IOException("Failed to add image record to database", e);
+ }
+ }
+
+ public void handleGetMedia(Context ctx) {
+ String uuidStr = ctx.pathParam("uuid");
+ UUID uuid;
+
+ try {
+ uuid = UUID.fromString(uuidStr);
+ } catch (IllegalArgumentException e) {
+ ctx.status(400).result("Invalid UUID format.");
+ return;
+ }
+
+ if (mediaRepo.getImage(uuid).isEmpty()) {
+ ctx.status(404).result("Image not found.");
+ return;
+ }
+
+ Path path = Path.of(appConfig.getStorageLocation(), "images", uuid + ".jpeg");
+
+ if (Files.exists(path)) {
+ ctx.contentType("image/jpeg");
+
+ try {
+ ctx.result(Files.newInputStream(path));
+ } catch (IOException e) {
+ ctx.status(500).result("Error reading image file.");
+ }
+ } else {
+ ctx.status(404).result("Image file missing from storage.");
+ }
+ }
+
+ public record ImageResponse(UUID uuid, String originalFilename, String contentType,
+ String timeUploaded) {
+ }
+
+
+ public void handleGetMediaIndex(Context ctx) {
+ List<ImageMetaData> metaDataList = mediaRepo.getAllImagesMetaData();
+
+ List<ImageResponse> responseList = metaDataList.stream().map(
+ meta -> new ImageResponse(meta.uuid(), meta.originalFilename(), meta.contentType(),
+ meta.timeUploaded().toString())).toList();
+
+ ctx.status(200).json(responseList);
+ }
+
+ public void handleDeleteMedia(Context ctx) {
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(ctx.pathParam("uuid"));
+ } catch (IllegalArgumentException e) {
+ ctx.status(400).result("Invalid UUID format.");
+ return;
+ }
+
+ try {
+ boolean deleted = mediaRepo.deleteImage(uuid);
+ if (!deleted) {
+ ctx.status(404).result("Image not found.");
+ return;
+ }
+ } catch (Exception e) {
+ ctx.status(500).result("Failed to delete image metadata from database.");
+ return;
+ }
+
+ Path path = Path.of(appConfig.getStorageLocation(), "images", uuid + ".jpeg");
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ ctx.status(500).result("Failed to delete image file from storage.");
+ return;
+ }
+
+ ctx.status(204);
+ }
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/PostController.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/PostController.java
new file mode 100644
index 0000000..93976fd
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/PostController.java
@@ -0,0 +1,31 @@
+package no.eliashaugsbakk.clams.server.controller;
+
+import io.javalin.http.Context;
+import no.eliashaugsbakk.clams.server.model.Post;
+import no.eliashaugsbakk.clams.server.model.PostDTO;
+import no.eliashaugsbakk.clams.server.repository.BlogPostRepo;
+import no.eliashaugsbakk.clams.server.service.SlugService;
+
+public class PostController {
+ private final BlogPostRepo blogPostRepo;
+ private final SlugService slugService;
+
+ public PostController(BlogPostRepo blogPostRepo, SlugService slugService) {
+ this.blogPostRepo = blogPostRepo;
+ this.slugService = slugService;
+ }
+
+ public void handlePostPost(Context ctx) {
+ PostDTO newPost = ctx.bodyAsClass(PostDTO.class);
+ blogPostRepo.addBlogPost(new Post(newPost, slugService.toSlug(newPost.title())));
+ }
+
+ public void handlePutPost(Context ctx) {
+ PostDTO updatedPost = ctx.bodyAsClass(PostDTO.class);
+ blogPostRepo.updateBlogPost(new Post(updatedPost, ctx.pathParam("slug")));
+ }
+
+ public void handleDeletePost(Context ctx) {
+ blogPostRepo.deleteBlogPost(ctx.pathParam("slug"));
+ }
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/ImageMetaData.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/ImageMetaData.java
new file mode 100644
index 0000000..e156d9a
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/ImageMetaData.java
@@ -0,0 +1,11 @@
+package no.eliashaugsbakk.clams.server.model;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public record ImageMetaData(
+ UUID uuid,
+ String originalFilename,
+ String contentType,
+ Instant timeUploaded
+) {}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/Post.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/Post.java
index f5e963f..4a7fcf6 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/Post.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/Post.java
@@ -2,5 +2,11 @@ package no.eliashaugsbakk.clams.server.model;
import java.time.Instant;
-public record Post(String title, String slug, String summary, Instant timePublished, String content, boolean isPublished) {
+public record Post(String title, String slug, String summary, Instant timePublished, String content,
+ boolean isPublished) {
+ public Post(PostDTO postDTO, String slug) {
+
+ this(postDTO.title(), slug, postDTO.summary(), Instant.now(), postDTO.content(),
+ postDTO.isPublished());
+ }
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostDTO.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostDTO.java
new file mode 100644
index 0000000..12254da
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostDTO.java
@@ -0,0 +1,4 @@
+package no.eliashaugsbakk.clams.server.model;
+
+public record PostDTO(String title, String summary, String content, boolean isPublished) {
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
index 8e43e2c..6455f7d 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
@@ -9,4 +9,9 @@ public interface BlogPostRepo {
List<PostMetaData> listPostsMetaData();
Optional<Post> getPost(String slug);
List<PostMetaData> searchPostsBody(String query);
+
+ void addBlogPost(Post post);
+ void updateBlogPost(Post post);
+ void deleteBlogPost(String slug);
+ boolean existsPostBySlug(String slug);
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
index 9da6818..754c915 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
@@ -119,4 +119,96 @@ public class BlogPostRepoSqlite implements BlogPostRepo {
throw new RepoException("Error searching posts for query: " + query, e);
}
}
+
+ @Override
+ public void addBlogPost(Post post) {
+ String sql = """
+ INSERT INTO posts (slug, title, content, summary, published, is_published)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, post.slug());
+ stmt.setString(2, post.title());
+ stmt.setString(3, post.content());
+ stmt.setString(4, post.summary());
+ stmt.setString(5, post.timePublished().toString());
+ stmt.setBoolean(6, post.isPublished());
+
+ stmt.executeUpdate();
+
+ } catch (SQLException e) {
+ throw new RepoException("Error adding blog post: " + post.slug(), e);
+ }
+ }
+
+ @Override
+ public void updateBlogPost(Post post) {
+ String sql = """
+ UPDATE posts
+ SET title = ?, content = ?, summary = ?, published = ?, last_edited = ?, is_published = ?
+ WHERE slug = ?
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, post.title());
+ stmt.setString(2, post.content());
+ stmt.setString(3, post.summary());
+ stmt.setString(4, post.timePublished().toString());
+ stmt.setString(5, Instant.now().toString());
+ stmt.setBoolean(6, post.isPublished());
+ stmt.setString(7, post.slug());
+
+ stmt.executeUpdate();
+
+ } catch (SQLException e) {
+ throw new RepoException("Error updating blog post: " + post.slug(), e);
+ }
+ }
+
+ @Override
+ public void deleteBlogPost(String slug) {
+ String sql = """
+ DELETE FROM posts
+ WHERE slug = ?
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, slug);
+
+ stmt.executeUpdate();
+
+ } catch (SQLException e) {
+ throw new RepoException("Error deleting blog post: " + slug, e);
+ }
+ }
+
+ @Override
+ public boolean existsPostBySlug(String slug) {
+ String sql = """
+ SELECT 1
+ FROM posts
+ WHERE slug = ?
+ LIMIT 1
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, slug);
+
+ try (ResultSet resultSet = stmt.executeQuery()) {
+ return resultSet.next();
+ }
+
+ } catch (SQLException e) {
+ throw new RepoException("Error checking existence of post by slug: " + slug, e);
+ }
+ }
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepo.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepo.java
new file mode 100644
index 0000000..163430c
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepo.java
@@ -0,0 +1,13 @@
+package no.eliashaugsbakk.clams.server.repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import no.eliashaugsbakk.clams.server.model.ImageMetaData;
+
+public interface MediaRepo {
+ void addImage(ImageMetaData image);
+ boolean deleteImage(UUID uuid);
+ Optional<ImageMetaData> getImage(UUID uuid);
+ List<ImageMetaData> getAllImagesMetaData();
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepoSqlite.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepoSqlite.java
new file mode 100644
index 0000000..143841c
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/MediaRepoSqlite.java
@@ -0,0 +1,120 @@
+package no.eliashaugsbakk.clams.server.repository;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import no.eliashaugsbakk.clams.server.model.ImageMetaData;
+
+public class MediaRepoSqlite implements MediaRepo {
+ private final SqliteManager manager;
+
+ public MediaRepoSqlite(SqliteManager sqliteManager) {
+ this.manager = sqliteManager;
+ }
+
+ @Override
+ public void addImage(ImageMetaData image) {
+ String sql = """
+ INSERT INTO images (uuid, original_filename, extension, time_uploaded)
+ VALUES (?, ?, ?, ?)
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, image.uuid().toString());
+ stmt.setString(2, image.originalFilename());
+ stmt.setString(3, image.contentType());
+ stmt.setString(4, image.timeUploaded().toString());
+
+ stmt.executeUpdate();
+
+ } catch (SQLException e) {
+ throw new RepoException("Error adding image: " + image.uuid(), e);
+ }
+ }
+
+ @Override
+ public boolean deleteImage(UUID uuid) {
+ String sql = """
+ DELETE FROM images
+ WHERE uuid = ?
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, uuid.toString());
+
+ int rowsAffected = stmt.executeUpdate();
+ return rowsAffected > 0;
+
+ } catch (SQLException e) {
+ throw new RepoException("Error deleting image: " + uuid, e);
+ }
+ }
+
+ @Override
+ public Optional<ImageMetaData> getImage(UUID uuid) {
+ String sql = """
+ SELECT uuid, original_filename, extension, time_uploaded
+ FROM images
+ WHERE uuid = ?
+ """;
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, uuid.toString());
+
+ try (ResultSet resultSet = stmt.executeQuery()) {
+ if (resultSet.next()) {
+ String originalFilename = resultSet.getString("original_filename");
+ String extension = resultSet.getString("extension");
+ Instant timeUploaded = Instant.parse(resultSet.getString("time_uploaded"));
+
+ return Optional.of(new ImageMetaData(uuid, originalFilename, extension, timeUploaded));
+ }
+ return Optional.empty();
+ }
+
+ } catch (SQLException e) {
+ throw new RepoException("Error fetching image: " + uuid, e);
+ }
+ }
+
+ @Override
+ public List<ImageMetaData> getAllImagesMetaData() {
+ String sql = """
+ SELECT uuid, original_filename, extension, time_uploaded
+ FROM images
+ """;
+
+ List<ImageMetaData> images = new ArrayList<>();
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ ResultSet resultSet = stmt.executeQuery()) {
+
+ while (resultSet.next()) {
+ UUID uuid = UUID.fromString(resultSet.getString("uuid"));
+ String originalFilename = resultSet.getString("original_filename");
+ String extension = resultSet.getString("extension");
+ Instant timeUploaded = Instant.parse(resultSet.getString("time_uploaded"));
+
+ images.add(new ImageMetaData(uuid, originalFilename, extension, timeUploaded));
+ }
+
+ return images;
+
+ } catch (SQLException e) {
+ throw new RepoException("Error fetching all images metadata", e);
+ }
+ }
+}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/SqliteManager.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/SqliteManager.java
index 6fc5daf..a192cee 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/SqliteManager.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/SqliteManager.java
@@ -59,12 +59,22 @@ public class SqliteManager implements AutoCloseable {
);
""";
+ String images = """
+ CREATE TABLE IF NOT EXISTS images (
+ uuid TEXT PRIMARY KEY,
+ original_filename TEXT NOT NULL,
+ extension TEXT,
+ time_uploaded TEXT NOT NULL
+ );
+ """;
+
try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute(posts);
stmt.execute(idxPublished);
stmt.execute(tags);
stmt.execute(post_tags);
+ stmt.execute(images);
} catch (SQLException e) {
throw new RepoException("Error while initializing database", e);
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/SlugService.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/SlugService.java
new file mode 100644
index 0000000..19af1da
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/SlugService.java
@@ -0,0 +1,42 @@
+package no.eliashaugsbakk.clams.server.service;
+
+import java.text.Normalizer;
+import java.util.Locale;
+import java.util.regex.Pattern;
+import no.eliashaugsbakk.clams.server.repository.BlogPostRepo;
+
+public class SlugService {
+ private final BlogPostRepo blogPostRepo;
+ private static final Pattern NON_LATIN = Pattern.compile("[^\\w-]");
+ private static final Pattern WHITESPACE = Pattern.compile("[\\s]");
+
+ public SlugService(BlogPostRepo blogPostRepo) {
+ this.blogPostRepo = blogPostRepo;
+ }
+
+ public String toSlug(String input) {
+ if (input == null) {
+ return "";
+ }
+
+ String slug = input.toLowerCase(Locale.ENGLISH);
+ slug = slug.replace("æ", "ae")
+ .replace("ø", "oe")
+ .replace("å", "aa");
+
+ slug = WHITESPACE.matcher(slug).replaceAll("-");
+ slug = Normalizer.normalize(slug, Normalizer.Form.NFD);
+ slug = NON_LATIN.matcher(slug).replaceAll("");
+
+ String finalSlug = slug;
+ int counter = 2;
+ while (true) {
+ if (blogPostRepo.existsPostBySlug(finalSlug)) {
+ finalSlug = slug + "-" + counter;
+ counter++;
+ } else {
+ return finalSlug;
+ }
+ }
+ }
+}