diff --git a/README.md b/README.md index 5d6ed92..3e808e4 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,10 @@ gleam test # Run the tests gleam shell # Run an Erlang shell ``` -## Flow - -### Upload - -take file upload --> get hash of file --> test against CSAM hash list --> generate UUID --> point UUID at hash of file --> save file if hash doesn't exist --> return UUID - -### Download - -take file UUID --> search for UUID in table and get hash --> return file if hash exists - ## TODO -- [] Handle file upload -- [] Handle file download -- [] Testing against CSAM hash list -- [] Logging IPs (up and down) of detected CSAM uploads +- [X] Handle link GET +- [X] Handle link POST +- [] Handle file POST +- [] Handle file GET +- [] Web Frontend diff --git a/src/app/config.gleam b/src/app/config.gleam index f13f9b9..8ffdc93 100644 --- a/src/app/config.gleam +++ b/src/app/config.gleam @@ -1,8 +1,13 @@ import envoy import gleam/int +import gleam/pgo import gleam/result.{unwrap} import wisp +pub type Context { + Context(db: pgo.Connection, config: Config) +} + pub type Config { Config( port: Int, diff --git a/src/app/router.gleam b/src/app/router.gleam index d7306f9..95910b5 100644 --- a/src/app/router.gleam +++ b/src/app/router.gleam @@ -1,12 +1,12 @@ -import app/config.{type Config} -import app/web.{type Context} +import app/config.{type Context} +import app/web import app/web/file import app/web/link import app/web/root import wisp.{type Request, type Response} -pub fn handle_request(req: Request, config: Config) -> Response { - use req: Request, ctx: Context <- web.middleware(req, config) +pub fn handle_request(req: Request, ctx: Context) -> Response { + use req: Request <- web.middleware(req) case wisp.path_segments(req) { ["f", ..] -> file.handle_request(req, ctx) diff --git a/src/app/web.gleam b/src/app/web.gleam index 3db4385..ff01b57 100644 --- a/src/app/web.gleam +++ b/src/app/web.gleam @@ -1,5 +1,3 @@ -import app/config.{type Config} -import gleam/pgo import wisp.{type Request, type Response} pub const html_head = " @@ -18,27 +16,13 @@ pub const html_tail = " " -pub type Context { - Context(db: pgo.Connection, config: Config) -} - pub fn middleware( req: Request, - config: Config, - handle_request: fn(Request, Context) -> Response, + handle_request: fn(Request) -> Response, ) -> Response { let req = wisp.method_override(req) use <- wisp.log_request(req) use <- wisp.rescue_crashes use req <- wisp.handle_head(req) - let db = - pgo.connect( - pgo.Config( - ..pgo.default_config(), - host: config.postgres_host, - database: config.postgres_db, - pool_size: config.postgres_pool_size, - ), - ) - handle_request(req, Context(db, config)) + handle_request(req) } diff --git a/src/app/web/file.gleam b/src/app/web/file.gleam index 13e4caf..928d37e 100644 --- a/src/app/web/file.gleam +++ b/src/app/web/file.gleam @@ -1,43 +1,49 @@ -import app/web.{type Context} +import app/config.{type Context} +import gleam/dynamic import gleam/http.{Get, Post} import gleam/list +import gleam/pgo +import gleam/result import gleam/string import wisp.{type Request, type Response} const schema = " -CREATE TABLE IF NOT EXISTS `FileReference` ( - nanoid TEXT <> - PathID INT -); -CREATE TABLE IF NOT EXISTS `FilePath` ( - PathID INT <> - md5_hash BLOB - banned BOOLEAN +CREATE TABLE IF NOT EXISTS \"File\" ( + nanoid VARCHAR(21) PRIMARY KEY, + md5_hash BYTEA ); " -type File { - Reference(md5_hash: BitArray, nanoid: String) - Path(md5_hash: String, banned: Bool) +pub type File { + File(md5_hash: BitArray, nanoid: String) } -fn hash_to_path(hash: String) -> List(String) { +pub fn prepare_database(ctx: Context) -> Result(Nil, pgo.QueryError) { + pgo.execute(schema, ctx.db, [], dynamic.dynamic) + |> result.map(fn(_) { Nil }) +} + +pub fn hash_to_path(hash: String) -> List(String) { list.new() |> list.append([hash |> string.slice(0, 2)]) |> list.append([hash |> string.slice(2, 2)]) |> list.append([hash |> string.slice(4, { hash |> string.length } - 4)]) } -pub fn handle_request(req: Request, ctx: Context) -> Response { - todo -} - -fn serve_file(req, uuid: String) -> Response { +fn handle_nano_id(req: Request, nano_id: String, ct: Context) -> Response { use <- wisp.require_method(req, Get) todo as "implement serve_file" } -fn receive_file(req: Request) -> Response { +fn handle_root(req: Request, ctx: Context) -> Response { use <- wisp.require_method(req, Post) todo as "implement receive_file" } + +pub fn handle_request(req: Request, ctx: Context) -> Response { + case wisp.path_segments(req) { + ["f"] -> handle_root(req, ctx) + ["f", nano_id] -> handle_nano_id(req, nano_id, ctx) + _ -> wisp.not_found() + } +} diff --git a/src/app/web/link.gleam b/src/app/web/link.gleam index b62ad5b..68045d7 100644 --- a/src/app/web/link.gleam +++ b/src/app/web/link.gleam @@ -1,4 +1,4 @@ -import app/web.{type Context} +import app/config.{type Context} import gleam/dynamic import gleam/http.{Get, Post} import gleam/int diff --git a/src/dumptruck.gleam b/src/dumptruck.gleam index 205e66a..97ddd3d 100644 --- a/src/dumptruck.gleam +++ b/src/dumptruck.gleam @@ -1,14 +1,33 @@ -import app/config +import app/config.{type Config, type Context, Context} import app/router import gleam/erlang/process +import gleam/pgo import mist import wisp -pub fn main() { - wisp.configure_logger() - let config = config.load_config_from_env() +fn with_context(config: Config, next: fn(Context) -> Nil) -> Nil { + let db = + pgo.connect( + pgo.Config( + ..pgo.default_config(), + host: config.postgres_host, + database: config.postgres_db, + pool_size: config.postgres_pool_size, + ), + ) + let ctx = Context(db: db, config: config) - let entrypoint = router.handle_request(_, config) + next(ctx) + + ctx.db |> pgo.disconnect +} + +pub fn main() { + let config = config.load_config_from_env() + use ctx: Context <- with_context(config) + + wisp.configure_logger() + let entrypoint = router.handle_request(_, ctx) let assert Ok(_) = wisp.mist_handler(entrypoint, config.secret_key_base) |> mist.new diff --git a/test/app/web/file_test.gleam b/test/app/web/file_test.gleam new file mode 100644 index 0000000..d7c3c0c --- /dev/null +++ b/test/app/web/file_test.gleam @@ -0,0 +1,68 @@ +import app/config.{type Context, Config, Context} +import app/web/file +import gleam/dynamic +import gleam/option.{None, Some} +import gleam/pgo +import gleam/string +import gleeunit/should + +const test_config = Config( + port: 8080, + max_bytes: 52_428_800, + url_root: "http://localhost", + secret_key_base: "cDv6ikrODZKjKbwSBBwspXInc61MYpCzQ8My9gW2QpQg3Y3lT7IJ5RJlzRN5HZK0", + postgres_db: "dumptruck", + postgres_user: "dumptruck", + postgres_password: "dumptruck", + postgres_host: "localhost", + postgres_pool_size: 15, + postgres_port: 5432, +) + +fn with_context(testcase: fn(Context) -> t) -> t { + let ctx = + Context( + db: pgo.Config( + ..pgo.default_config(), + database: test_config.postgres_db, + password: test_config.postgres_password |> Some, + user: test_config.postgres_user, + host: test_config.postgres_host, + pool_size: test_config.postgres_pool_size, + port: 5432, + ) + |> pgo.connect, + config: test_config, + ) + + case file.prepare_database(ctx) { + Ok(_) -> { + let ret = testcase(ctx) + + ctx.db |> pgo.disconnect + + ret + } + Error(e) -> { + e |> string.inspect |> panic + } + } +} + +pub fn null_test() { + use ctx <- with_context() + + pgo.execute( + "select $1", + ctx.db, + [pgo.null()], + dynamic.element(0, dynamic.optional(dynamic.int)), + ) + |> should.equal(Ok(pgo.Returned(count: 1, rows: [None]))) +} + +pub fn hash_to_path_test() { + "f7b478961451483b8c251c788750d5ef" + |> file.hash_to_path + |> should.equal(["f7", "b4", "78961451483b8c251c788750d5ef"]) +} diff --git a/test/app/web/link_test.gleam b/test/app/web/link_test.gleam index 1dfdaec..f229ffd 100644 --- a/test/app/web/link_test.gleam +++ b/test/app/web/link_test.gleam @@ -1,8 +1,6 @@ -import app/config.{Config} -import app/web.{type Context, Context} +import app/config.{type Context, Config, Context} import app/web/link.{Link} import gleam/dynamic -import gleam/io import gleam/option.{None, Some} import gleam/pgo import gleam/result