diff --git a/src/app/config.gleam b/src/app/config.gleam index 8ffdc93..5e389aa 100644 --- a/src/app/config.gleam +++ b/src/app/config.gleam @@ -2,6 +2,7 @@ import envoy import gleam/int import gleam/pgo import gleam/result.{unwrap} +import gleam/uri import wisp pub type Context { @@ -34,6 +35,23 @@ pub fn get_string_or(var: String, default: String) -> String { envoy.get(var) |> unwrap(default) } +pub fn generate_url( + nano_id: String, + root: String, + ctx: Context, +) -> Result(String, Nil) { + uri.parse( + ctx.config.url_root + <> ":" + <> ctx.config.port |> int.to_string + <> "/" + <> root + <> "/" + <> nano_id, + ) + |> result.map(uri.to_string) +} + // fn get_path_or(var: String, default: List(String)) -> List(String) { // case envoy.get(var) { // Ok(path_raw) -> { diff --git a/src/app/web/file.gleam b/src/app/web/file.gleam index 928d37e..e1d08d6 100644 --- a/src/app/web/file.gleam +++ b/src/app/web/file.gleam @@ -1,21 +1,25 @@ import app/config.{type Context} +import gleam/bit_array +import gleam/crypto import gleam/dynamic import gleam/http.{Get, Post} import gleam/list import gleam/pgo import gleam/result import gleam/string +import ids/nanoid import wisp.{type Request, type Response} const schema = " CREATE TABLE IF NOT EXISTS \"File\" ( nanoid VARCHAR(21) PRIMARY KEY, - md5_hash BYTEA + Md5Hash VARCHAR(32) + FileName TEXT ); " pub type File { - File(md5_hash: BitArray, nanoid: String) + File(nano_id: String, md5_hash: String, file_name: String) } pub fn prepare_database(ctx: Context) -> Result(Nil, pgo.QueryError) { @@ -23,27 +27,95 @@ pub fn prepare_database(ctx: Context) -> Result(Nil, pgo.QueryError) { |> 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 store(file: File, ctx: Context) -> Result(String, Nil) { + let sql = + "INSERT INTO \"File\" (NanoID, Md5Hash) VALUES ($1, $2) RETURNING NanoID;" + case + pgo.execute( + sql, + ctx.db, + [pgo.text(file.nano_id), pgo.text(file.md5_hash)], + dynamic.element(0, dynamic.string), + ) + { + Ok(target_id) -> + target_id.rows |> list.first |> result.map_error(fn(_) { Nil }) + Error(_) -> Error(Nil) + } } -fn handle_nano_id(req: Request, nano_id: String, ct: Context) -> Response { +pub fn retrieve(nano_id: String, ctx: Context) -> Result(File, Nil) { + let sql = "SELECT NanoID,Md5Hash,FileName FROM \"File\" WHERE NanoID = $1;" + case + pgo.execute( + sql, + ctx.db, + [pgo.text(nano_id)], + dynamic.tuple3(dynamic.string, dynamic.string, dynamic.string), + ) + { + Ok(res) -> + case res.rows |> list.first { + Ok(#(nano_id, md5_hash, file_name)) -> + Ok(File(nano_id, md5_hash, file_name)) + _ -> Error(Nil) + } + _ -> Error(Nil) + } +} + +pub fn hash_to_path(hash: String) -> String { + [""] + |> list.append([string.slice(hash, 0, 2)]) + |> list.append([string.slice(hash, 2, 2)]) + |> list.append([string.slice(hash, 4, string.length(hash) - 4)]) + |> string.join("/") +} + +pub fn serve_hash(req: Request, md5_hash: String) -> Response { use <- wisp.require_method(req, Get) - todo as "implement serve_file" + let path = hash_to_path(md5_hash) + // TODO: Test mimetype + wisp.ok() + |> wisp.set_header("Content-Type", "application/octet-stream") + |> wisp.file_download(named: "blob", from: path) } -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 { +pub fn handle_get(req: Request, ctx: Context) -> Response { + use <- wisp.require_method(req, Get) case wisp.path_segments(req) { - ["f"] -> handle_root(req, ctx) - ["f", nano_id] -> handle_nano_id(req, nano_id, ctx) + ["f", nano_id] -> + case retrieve(nano_id, ctx) { + Ok(file) -> serve_hash(req, file.md5_hash) + Error(_) -> wisp.not_found() + } + _ -> wisp.not_found() + } +} + +pub fn handle_post(req: Request, ctx: Context) -> Response { + use <- wisp.require_method(req, Post) + use body <- wisp.require_bit_array_body(req) + + case wisp.path_segments(req) { + ["f", path] -> { + let nano_id = nanoid.generate() + let md5_hash = crypto.hash(crypto.Md5, body) |> bit_array.base16_encode + let file = File(nano_id, md5_hash, path) + + case store(file, ctx) |> result.try(config.generate_url(_, "f", ctx)) { + Ok(url) -> wisp.ok() |> wisp.string_body(url) + Error(_) -> wisp.internal_server_error() + } + } + _ -> wisp.not_found() + } +} + +pub fn handle_request(req: Request, ctx: Context) -> Response { + case req.method { + Get -> handle_get(req, ctx) + Post -> handle_post(req, ctx) _ -> wisp.not_found() } } diff --git a/src/app/web/link.gleam b/src/app/web/link.gleam index 68045d7..1e1df26 100644 --- a/src/app/web/link.gleam +++ b/src/app/web/link.gleam @@ -1,15 +1,10 @@ import app/config.{type Context} import gleam/dynamic import gleam/http.{Get, Post} -import gleam/int -import gleam/io import gleam/list -import gleam/option.{type Option, None, Some} import gleam/pair import gleam/pgo import gleam/result -import gleam/string -import gleam/string_builder import gleam/uri.{type Uri} import ids/nanoid import wisp.{type Request, type Response} @@ -30,7 +25,7 @@ pub fn prepare_database(ctx: Context) -> Result(Nil, pgo.QueryError) { |> result.map(fn(_) { Nil }) } -pub fn store(link: Link, ctx: Context) -> Result(String, Option(pgo.QueryError)) { +pub fn store(link: Link, ctx: Context) -> Result(String, Nil) { let sql = "INSERT INTO \"Link\" (NanoID, TargetURL) VALUES ($1, $2) RETURNING NanoID;" case @@ -42,8 +37,8 @@ pub fn store(link: Link, ctx: Context) -> Result(String, Option(pgo.QueryError)) ) { Ok(target_id) -> - target_id.rows |> list.first |> result.map_error(fn(_) { None }) - Error(e) -> Error(Some(e)) + target_id.rows |> list.first |> result.map_error(fn(_) { Nil }) + Error(_) -> Error(Nil) } } @@ -58,55 +53,26 @@ pub fn retrieve(nano_id: String, ctx: Context) -> Result(Link, Nil) { ) { Ok(res) -> { - let target_raw = res.rows |> list.first - let nano_id = - target_raw - |> result.map(pair.first) + let sql_response = res.rows |> list.first + let nano_id = sql_response |> result.map(pair.first) let target_url = - target_raw - |> result.map(pair.second) - |> result.try(uri.parse) + sql_response |> result.map(pair.second) |> result.try(uri.parse) - use nano_id <- result.try(nano_id) - use target_url <- result.try(target_url) - - Ok(Link(nano_id, target_url)) + { + use nano_id <- result.try(nano_id) + use target_url <- result.try(target_url) + Ok(Link(nano_id, target_url)) + } } _ -> Error(Nil) } } -pub fn generate_url( - nano_id: Result(String, Nil), - ctx: Context, -) -> Result(Uri, Nil) { - use nano_id <- result.try(nano_id) - uri.parse( - ctx.config.url_root - <> ":" - <> ctx.config.port |> int.to_string - <> "/l/" - <> nano_id, - ) -} - pub fn handle_uri(url: Uri, ctx: Context) -> Response { - nanoid.generate() - |> Link(url) - |> store(ctx) - |> result.map_error(fn(error) { - error |> string.inspect |> io.debug - Nil - }) - |> generate_url(ctx) - |> result.map(fn(uri) { - uri - |> uri.to_string - |> string_builder.from_string - |> wisp.Text - |> wisp.set_body(wisp.response(200), _) - }) - |> result.unwrap(wisp.response(404)) + store(nanoid.generate() |> Link(url), ctx) + |> result.try(config.generate_url(_, "l", ctx)) + |> result.map(wisp.string_body(wisp.response(200), _)) + |> result.unwrap(wisp.response(500)) } pub fn handle_root(req: Request, ctx: Context) -> Response { diff --git a/src/dumptruck.gleam b/src/dumptruck.gleam index 97ddd3d..92ee748 100644 --- a/src/dumptruck.gleam +++ b/src/dumptruck.gleam @@ -5,7 +5,8 @@ import gleam/pgo import mist import wisp -fn with_context(config: Config, next: fn(Context) -> Nil) -> Nil { +fn with_context(continue: fn(Context) -> Nil) -> Nil { + let config = config.load_config_from_env() let db = pgo.connect( pgo.Config( @@ -17,19 +18,19 @@ fn with_context(config: Config, next: fn(Context) -> Nil) -> Nil { ) let ctx = Context(db: db, config: config) - next(ctx) + continue(ctx) ctx.db |> pgo.disconnect } pub fn main() { - let config = config.load_config_from_env() - use ctx: Context <- with_context(config) + use ctx <- with_context() wisp.configure_logger() - let entrypoint = router.handle_request(_, ctx) + let assert Ok(_) = - wisp.mist_handler(entrypoint, config.secret_key_base) + router.handle_request(_, ctx) + |> wisp.mist_handler(config.secret_key_base) |> mist.new |> mist.port(config.port) |> mist.start_http