215 lines
6.1 KiB
Rust
215 lines
6.1 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use std::{
|
|
borrow::Cow,
|
|
io::{self, Cursor, Read},
|
|
path::PathBuf,
|
|
};
|
|
|
|
use ahash::AHashMap;
|
|
use arc_swap::ArcSwap;
|
|
use store::BlobStore;
|
|
|
|
use crate::Core;
|
|
|
|
use super::WEBADMIN_KEY;
|
|
|
|
pub struct WebAdminManager {
|
|
bundle_path: TempDir,
|
|
routes: ArcSwap<AHashMap<String, Resource<PathBuf>>>,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct Resource<T> {
|
|
pub content_type: Cow<'static, str>,
|
|
pub contents: T,
|
|
}
|
|
|
|
impl<T> Resource<T> {
|
|
pub fn new(content_type: impl Into<Cow<'static, str>>, contents: T) -> Self {
|
|
Self {
|
|
content_type: content_type.into(),
|
|
contents,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WebAdminManager {
|
|
pub fn new(base_path: PathBuf) -> Self {
|
|
Self {
|
|
bundle_path: TempDir::new(base_path),
|
|
routes: ArcSwap::from_pointee(Default::default()),
|
|
}
|
|
}
|
|
|
|
pub async fn get(&self, path: &str) -> trc::Result<Resource<Vec<u8>>> {
|
|
let routes = self.routes.load();
|
|
if let Some(resource) = routes.get(path).or_else(|| routes.get("index.html")) {
|
|
tokio::fs::read(&resource.contents)
|
|
.await
|
|
.map(|contents| Resource {
|
|
content_type: resource.content_type.clone(),
|
|
contents,
|
|
})
|
|
.map_err(|err| {
|
|
trc::ResourceEvent::Error
|
|
.reason(err)
|
|
.ctx(trc::Key::Path, path.to_string())
|
|
.caused_by(trc::location!())
|
|
})
|
|
} else {
|
|
Ok(Resource::default())
|
|
}
|
|
}
|
|
|
|
pub async fn unpack(&self, blob_store: &BlobStore) -> trc::Result<()> {
|
|
// Delete any existing bundles
|
|
self.bundle_path.clean().await.map_err(unpack_error)?;
|
|
|
|
// Obtain webadmin bundle
|
|
let bundle = blob_store
|
|
.get_blob(WEBADMIN_KEY, 0..usize::MAX)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
trc::ResourceEvent::NotFound
|
|
.caused_by(trc::location!())
|
|
.details("Webadmin bundle not found")
|
|
})?;
|
|
|
|
// Uncompress
|
|
let mut bundle = zip::ZipArchive::new(Cursor::new(bundle)).map_err(|err| {
|
|
trc::ResourceEvent::Error
|
|
.caused_by(trc::location!())
|
|
.reason(err)
|
|
.details("Failed to decompress webadmin bundle")
|
|
})?;
|
|
let mut routes = AHashMap::new();
|
|
for i in 0..bundle.len() {
|
|
let (file_name, contents) = {
|
|
let mut file = bundle.by_index(i).map_err(|err| {
|
|
trc::ResourceEvent::Error
|
|
.caused_by(trc::location!())
|
|
.reason(err)
|
|
.details("Failed to read file from webadmin bundle")
|
|
})?;
|
|
if file.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let mut contents = Vec::new();
|
|
file.read_to_end(&mut contents).map_err(unpack_error)?;
|
|
(file.name().to_string(), contents)
|
|
};
|
|
let path = self.bundle_path.path.join(format!("{i:02}"));
|
|
tokio::fs::write(&path, contents)
|
|
.await
|
|
.map_err(unpack_error)?;
|
|
|
|
let resource = Resource {
|
|
content_type: match file_name
|
|
.rsplit_once('.')
|
|
.map(|(_, ext)| ext)
|
|
.unwrap_or_default()
|
|
{
|
|
"html" => "text/html",
|
|
"css" => "text/css",
|
|
"wasm" => "application/wasm",
|
|
"js" => "application/javascript",
|
|
"json" => "application/json",
|
|
"png" => "image/png",
|
|
"svg" => "image/svg+xml",
|
|
"ico" => "image/x-icon",
|
|
_ => "application/octet-stream",
|
|
}
|
|
.into(),
|
|
contents: path,
|
|
};
|
|
|
|
routes.insert(file_name, resource);
|
|
}
|
|
|
|
// Update routes
|
|
self.routes.store(routes.into());
|
|
|
|
trc::event!(
|
|
Resource(trc::ResourceEvent::WebadminUnpacked),
|
|
Path = self.bundle_path.path.to_string_lossy().into_owned(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update(&self, core: &Core) -> trc::Result<()> {
|
|
let bytes = core
|
|
.storage
|
|
.config
|
|
.fetch_resource("webadmin")
|
|
.await
|
|
.map_err(|err| {
|
|
trc::ResourceEvent::Error
|
|
.caused_by(trc::location!())
|
|
.reason(err)
|
|
.details("Failed to download webadmin")
|
|
})?;
|
|
core.storage.blob.put_blob(WEBADMIN_KEY, &bytes).await
|
|
}
|
|
|
|
pub async fn update_and_unpack(&self, core: &Core) -> trc::Result<()> {
|
|
self.update(core).await?;
|
|
self.unpack(&core.storage.blob).await
|
|
}
|
|
}
|
|
|
|
impl Resource<Vec<u8>> {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.content_type.is_empty() && self.contents.is_empty()
|
|
}
|
|
}
|
|
|
|
pub struct TempDir {
|
|
pub path: PathBuf,
|
|
}
|
|
|
|
impl TempDir {
|
|
pub fn new(path: PathBuf) -> TempDir {
|
|
TempDir {
|
|
path: path.join(std::str::from_utf8(WEBADMIN_KEY).unwrap()),
|
|
}
|
|
}
|
|
|
|
pub async fn clean(&self) -> io::Result<()> {
|
|
if tokio::fs::metadata(&self.path).await.is_ok() {
|
|
let _ = tokio::fs::remove_dir_all(&self.path).await;
|
|
}
|
|
tokio::fs::create_dir(&self.path).await
|
|
}
|
|
}
|
|
|
|
fn unpack_error(err: std::io::Error) -> trc::Error {
|
|
trc::ResourceEvent::Error
|
|
.reason(err)
|
|
.details("Failed to unpack webadmin bundle")
|
|
}
|
|
|
|
impl Default for WebAdminManager {
|
|
fn default() -> Self {
|
|
Self::new(std::env::temp_dir())
|
|
}
|
|
}
|
|
|
|
impl Default for TempDir {
|
|
fn default() -> Self {
|
|
Self::new(std::env::temp_dir())
|
|
}
|
|
}
|
|
|
|
impl Drop for TempDir {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.path);
|
|
}
|
|
}
|