| 166f6e9b Andy McCall |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use super::{caller, not_found, ApiError}; |
| 8 |
use crate::db; |
| 9 |
use crate::drops::{self, DropError, DropFile, NewFile}; |
| 10 |
use crate::Shared; |
| 11 |
use axum::extract::{Path, State}; |
| 12 |
use axum::http::{HeaderMap, StatusCode}; |
| 13 |
use axum::Json; |
| 14 |
use hub_api::{Drop, DropEdit, NewDrop}; |
| 15 |
|
| 16 |
impl From<DropError> for ApiError { |
| 17 |
fn from(e: DropError) -> ApiError { |
| 18 |
match e { |
| 19 |
DropError::Bad(m) => ApiError(StatusCode::UNPROCESSABLE_ENTITY, m), |
| 20 |
DropError::Repo(e) => e.into(), |
| 21 |
DropError::Db(e) => e.into(), |
| 22 |
} |
| 23 |
} |
| 24 |
} |
| 25 |
|
| 26 |
|
| 27 |
fn shape(app: &Shared, row: &db::repos::RepoRow, files: &[DropFile], content: bool) -> Drop { |
| 28 |
let base = app.config.base_url(); |
| 29 |
let url = drops::url_of(row); |
| 30 |
Drop { |
| 31 |
id: drops::id_of(row).to_string(), |
| 32 |
owner: row.owner.clone(), |
| 33 |
title: row.title.clone(), |
| 34 |
description: row.description.clone(), |
| 35 |
visibility: drops::visibility_label(row).to_string(), |
| 36 |
url: format!("{base}{url}"), |
| 37 |
clone_url: format!("{base}{url}"), |
| 38 |
created_ms: row.created_ms, |
| 39 |
updated_ms: row.updated_ms, |
| 40 |
files: files |
| 41 |
.iter() |
| 42 |
.map(|f| hub_api::DropFile { |
| 43 |
name: f.path.clone(), |
| 44 |
size: f.size, |
| 45 |
content: if content { f.text.clone() } else { None }, |
| 46 |
raw_url: format!("{base}{url}/raw/{}/{}", row.default_bookmark, f.path), |
| 47 |
}) |
| 48 |
.collect(), |
| 49 |
} |
| 50 |
} |
| 51 |
|
| 52 |
async fn listed(app: &Shared, rows: Vec<db::repos::RepoRow>) -> Result<Vec<Drop>, ApiError> { |
| 53 |
let mut out = Vec::with_capacity(rows.len()); |
| 54 |
for row in rows { |
| 55 |
let files = drops::files(app, &row, "").await.map(|(_, f)| f).unwrap_or_default(); |
| 56 |
out.push(shape(app, &row, &files, false)); |
| 57 |
} |
| 58 |
Ok(out) |
| 59 |
} |
| 60 |
|
| 61 |
|
| 62 |
pub(super) async fn mine(State(app): State<Shared>, headers: HeaderMap) -> Result<Json<Vec<Drop>>, ApiError> { |
| 63 |
let c = caller(&app, &headers).await?; |
| 64 |
let me = c.require("repo:read")?.clone(); |
| 65 |
let rows = db::repos::drops_of(&app.db, &me.username).await?; |
| 66 |
Ok(Json(listed(&app, rows).await?)) |
| 67 |
} |
| 68 |
|
| 69 |
|
| 70 |
|
| 71 |
pub(super) async fn of_user(State(app): State<Shared>, headers: HeaderMap, Path(name): Path<String>) -> Result<Json<Vec<Drop>>, ApiError> { |
| 72 |
let c = caller(&app, &headers).await?; |
| 73 |
let account = db::accounts::account_by_name(&app.db, &name).await?.ok_or_else(|| not_found("no such user"))?; |
| 74 |
let rows: Vec<_> = |
| 75 |
db::repos::drops_of(&app.db, &account.username).await?.into_iter().filter(|r| drops::listed_to(c.account.as_ref(), r)).collect(); |
| 76 |
Ok(Json(listed(&app, rows).await?)) |
| 77 |
} |
| 78 |
|
| 79 |
|
| 80 |
pub(super) async fn create( |
| 81 |
State(app): State<Shared>, |
| 82 |
headers: HeaderMap, |
| 83 |
Json(body): Json<NewDrop>, |
| 84 |
) -> Result<(StatusCode, Json<Drop>), ApiError> { |
| 85 |
let c = caller(&app, &headers).await?; |
| 86 |
let me = c.require("repo:write")?.clone(); |
| 87 |
let files: Vec<NewFile> = body.files.into_iter().map(|f| NewFile { name: f.name, content: f.content }).collect(); |
| 88 |
let row = drops::create(&app, &me, &body.title, &body.description, body.secret, &files).await?; |
| 89 |
let (_, files) = drops::files(&app, &row, "").await?; |
| 90 |
Ok((StatusCode::CREATED, Json(shape(&app, &row, &files, true)))) |
| 91 |
} |
| 92 |
|
| 93 |
async fn row_of(app: &Shared, id: &str) -> Result<db::repos::RepoRow, ApiError> { |
| 94 |
if !crate::repos::names::valid_drop_id(id) { |
| 95 |
return Err(not_found("no such drop")); |
| 96 |
} |
| 97 |
db::repos::drop_by_id(&app.db, id).await?.ok_or_else(|| not_found("no such drop")) |
| 98 |
} |
| 99 |
|
| 100 |
|
| 101 |
pub(super) async fn view(State(app): State<Shared>, headers: HeaderMap, Path(id): Path<String>) -> Result<Json<Drop>, ApiError> { |
| 102 |
let c = caller(&app, &headers).await?; |
| 103 |
let row = row_of(&app, &id).await?; |
| 104 |
if c.account.as_ref().is_some_and(|a| a.blocked()) { |
| 105 |
return Err(not_found("no such drop")); |
| 106 |
} |
| 107 |
let (_, files) = drops::files(&app, &row, "").await?; |
| 108 |
Ok(Json(shape(&app, &row, &files, true))) |
| 109 |
} |
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
async fn writable( |
| 114 |
app: &Shared, |
| 115 |
headers: &HeaderMap, |
| 116 |
id: &str, |
| 117 |
deleting: bool, |
| 118 |
) -> Result<(db::accounts::Account, db::repos::RepoRow), ApiError> { |
| 119 |
let c = caller(app, headers).await?; |
| 120 |
let me = c.require("repo:write")?.clone(); |
| 121 |
let row = row_of(app, id).await?; |
| 122 |
let may = if deleting { drops::may_delete(Some(&me), &row) } else { drops::may_edit(Some(&me), &row) }; |
| 123 |
if !may { |
| 124 |
return Err(super::forbidden(if deleting { |
| 125 |
"only the drop's owner or a site administrator may delete it" |
| 126 |
} else { |
| 127 |
"only the drop's owner may change it" |
| 128 |
})); |
| 129 |
} |
| 130 |
Ok((me, row)) |
| 131 |
} |
| 132 |
|
| 133 |
|
| 134 |
pub(super) async fn edit( |
| 135 |
State(app): State<Shared>, |
| 136 |
headers: HeaderMap, |
| 137 |
Path(id): Path<String>, |
| 138 |
Json(body): Json<DropEdit>, |
| 139 |
) -> Result<Json<Drop>, ApiError> { |
| 140 |
let (me, row) = writable(&app, &headers, &id, false).await?; |
| 141 |
let mut seen = Vec::new(); |
| 142 |
let mut files = Vec::with_capacity(body.files.len()); |
| 143 |
for f in body.files { |
| 144 |
let name = drops::check_name(&f.name).map_err(|m| ApiError(StatusCode::UNPROCESSABLE_ENTITY, m))?; |
| 145 |
if seen.contains(&name) { |
| 146 |
return Err(ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{name} is named twice."))); |
| 147 |
} |
| 148 |
seen.push(name.clone()); |
| 149 |
files.push(oxy_core::repo::FileEdit { path: name, content: f.content.map(|c| c.replace("\r\n", "\n").into_bytes()) }); |
| 150 |
} |
| 151 |
let update = drops::Update { |
| 152 |
title: body.title.unwrap_or_else(|| row.title.clone()), |
| 153 |
description: body.description.unwrap_or_else(|| row.description.clone()), |
| 154 |
secret: body.secret.unwrap_or(row.visibility() != hub_api::Visibility::Public), |
| 155 |
files, |
| 156 |
message: body.message.unwrap_or_default(), |
| 157 |
}; |
| 158 |
drops::update(&app, &me, &row, update).await?; |
| 159 |
let row = db::repos::get_by_id(&app.db, row.id).await?.ok_or_else(|| not_found("no such drop"))?; |
| 160 |
let (_, files) = drops::files(&app, &row, "").await?; |
| 161 |
Ok(Json(shape(&app, &row, &files, true))) |
| 162 |
} |
| 163 |
|
| 164 |
|
| 165 |
pub(super) async fn delete(State(app): State<Shared>, headers: HeaderMap, Path(id): Path<String>) -> Result<StatusCode, ApiError> { |
| 166 |
let (me, row) = writable(&app, &headers, &id, true).await?; |
| 167 |
drops::delete(&app, &me, &row).await?; |
| 168 |
Ok(StatusCode::NO_CONTENT) |
| 169 |
} |