Add privileged purging, alias <-> filename relation queries

This commit is contained in:
asonix 2020-07-11 17:22:45 -05:00
parent fd809e4a0b
commit 7c0a407568
3 changed files with 133 additions and 6 deletions

View file

@ -120,15 +120,25 @@ pict-rs offers four endpoints:
which would create a 256x256px JPEG thumbnail and blur it which would create a 256x256px JPEG thumbnail and blur it
- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to - `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON
- `POST /internal/import` for uploading an image while preserving the filename. This should not be
exposed to the public internet, as it can cause naming conflicts with saved files. The upload
format and response format are the same as the `POST /image` endpoint.
This endpoint also requires authentication via the `X-Api-Token` header, and is disabled unless
the `--api-key` option is passed to the binary or the PICTRS_API_KEY environment variable is The following endpoints are protected by an API key via the `X-Api-Token` header, and are disabled
unless the `--api-key` option is passed to the binary or the PICTRS_API_KEY environment variable is
set. set.
A secure API key can be generated by any password generator. A secure API key can be generated by any password generator.
- `POST /internal/import` for uploading an image while preserving the filename. This should not be
exposed to the public internet, as it can cause naming conflicts with saved files. The upload
format and response format are the same as the `POST /image` endpoint.
- `POST /internal/purge?...` Purge a file by it's filename or alias. This removes all aliases and
files associated with the query.
- `?file=asdf.png` purge by filename
- `?alias=asdf.png` purge by alias
- `GET /internal/aliases?...` Get the aliases for a file by it's filename or alias
- `?file={filename}` get aliases by filename
- `?alias={alias}` get aliases by alias
- `GET /internal/filename?alias={alias}` Get the filename for a file by it's alias
## Contributing ## Contributing
Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3. Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3.

View file

@ -396,6 +396,66 @@ where
.streaming(stream.err_into()) .streaming(stream.err_into())
} }
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
enum FileOrAlias {
File { file: String },
Alias { alias: String },
}
async fn purge(
query: web::Query<FileOrAlias>,
upload_manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> {
let aliases = match query.into_inner() {
FileOrAlias::File { file } => upload_manager.aliases_by_filename(file).await?,
FileOrAlias::Alias { alias } => upload_manager.aliases_by_alias(alias).await?,
};
for alias in aliases.iter() {
upload_manager
.delete_without_token(alias.to_owned())
.await?;
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"msg": "ok",
"aliases": aliases
})))
}
async fn aliases(
query: web::Query<FileOrAlias>,
upload_manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> {
let aliases = match query.into_inner() {
FileOrAlias::File { file } => upload_manager.aliases_by_filename(file).await?,
FileOrAlias::Alias { alias } => upload_manager.aliases_by_alias(alias).await?,
};
Ok(HttpResponse::Ok().json(serde_json::json!({
"msg": "ok",
"aliases": aliases,
})))
}
#[derive(Debug, serde::Deserialize)]
struct ByAlias {
alias: String,
}
async fn filename_by_alias(
query: web::Query<ByAlias>,
upload_manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> {
let filename = upload_manager.from_alias(query.into_inner().alias).await?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"msg": "ok",
"filename": filename,
})))
}
#[actix_rt::main] #[actix_rt::main]
async fn main() -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> {
MAGICK_INIT.call_once(|| { MAGICK_INIT.call_once(|| {
@ -508,7 +568,10 @@ async fn main() -> Result<(), anyhow::Error> {
web::resource("/import") web::resource("/import")
.wrap(import_form.clone()) .wrap(import_form.clone())
.route(web::post().to(upload)), .route(web::post().to(upload)),
), )
.service(web::resource("/purge").route(web::post().to(purge)))
.service(web::resource("/aliases").route(web::get().to(aliases)))
.service(web::resource("/filename").route(web::get().to(filename_by_alias))),
) )
}) })
.bind(CONFIG.bind_address())? .bind(CONFIG.bind_address())?

View file

@ -135,6 +135,60 @@ impl UploadManager {
Ok(()) Ok(())
} }
/// Get a list of aliases for a given file
pub(crate) async fn aliases_by_filename(
&self,
filename: String,
) -> Result<Vec<String>, UploadError> {
let fname_tree = self.inner.filename_tree.clone();
let hash = web::block(move || fname_tree.get(filename.as_bytes()))
.await?
.ok_or(UploadError::MissingAlias)?;
self.aliases_by_hash(&hash).await
}
/// Get a list of aliases for a given alias
pub(crate) async fn aliases_by_alias(&self, alias: String) -> Result<Vec<String>, UploadError> {
let alias_tree = self.inner.alias_tree.clone();
let hash = web::block(move || alias_tree.get(alias.as_bytes()))
.await?
.ok_or(UploadError::MissingFilename)?;
self.aliases_by_hash(&hash).await
}
async fn aliases_by_hash(&self, hash: &sled::IVec) -> Result<Vec<String>, UploadError> {
let (start, end) = alias_key_bounds(hash);
let db = self.inner.db.clone();
let aliases =
web::block(move || db.range(start..end).values().collect::<Result<Vec<_>, _>>())
.await?;
debug!("Got {} aliases for hash", aliases.len());
let aliases = aliases
.into_iter()
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
.collect::<Vec<_>>();
for alias in aliases.iter() {
debug!("{}", alias);
}
Ok(aliases)
}
/// Delete an alias without a delete token
pub(crate) async fn delete_without_token(&self, alias: String) -> Result<(), UploadError> {
let token_key = delete_key(&alias);
let alias_tree = self.inner.alias_tree.clone();
let token = web::block(move || alias_tree.get(token_key.as_bytes()))
.await?
.ok_or(UploadError::MissingAlias)?;
self.delete(alias, String::from_utf8(token.to_vec())?).await
}
/// Delete the alias, and the file & variants if no more aliases exist /// Delete the alias, and the file & variants if no more aliases exist
#[instrument(skip(self, alias, token))] #[instrument(skip(self, alias, token))]
pub(crate) async fn delete(&self, alias: String, token: String) -> Result<(), UploadError> { pub(crate) async fn delete(&self, alias: String, token: String) -> Result<(), UploadError> {