mirror of
https://git.asonix.dog/asonix/pict-rs.git
synced 2024-11-24 18:41:06 +00:00
Add privileged purging, alias <-> filename relation queries
This commit is contained in:
parent
fd809e4a0b
commit
7c0a407568
3 changed files with 133 additions and 6 deletions
20
README.md
20
README.md
|
@ -120,15 +120,25 @@ pict-rs offers four endpoints:
|
|||
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 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.
|
||||
|
||||
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
|
||||
Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3.
|
||||
|
|
65
src/main.rs
65
src/main.rs
|
@ -396,6 +396,66 @@ where
|
|||
.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]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
MAGICK_INIT.call_once(|| {
|
||||
|
@ -508,7 +568,10 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
web::resource("/import")
|
||||
.wrap(import_form.clone())
|
||||
.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())?
|
||||
|
|
|
@ -135,6 +135,60 @@ impl UploadManager {
|
|||
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
|
||||
#[instrument(skip(self, alias, token))]
|
||||
pub(crate) async fn delete(&self, alias: String, token: String) -> Result<(), UploadError> {
|
||||
|
|
Loading…
Reference in a new issue