mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +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
|
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.
|
||||||
|
|
65
src/main.rs
65
src/main.rs
|
@ -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())?
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue