aboutsummaryrefslogtreecommitdiffstats
path: root/src/handlers.rs
blob: 8b4d6e5fd2b5a58c608dfb4995cbb81680f5ea1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use std::time::Duration;

use actix_web::{HttpRequest, HttpResponse, Responder, http::header::ContentType, web};
use actix_web_lab::sse;
use bytesize::ByteSize;
use dav_server::{
    DavConfig, DavHandler,
    actix::{DavRequest, DavResponse},
};
use log::{error, info, warn};
use percent_encoding::percent_decode_str;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tokio::task::JoinSet;

use crate::{config::MiniserveConfig, errors::RuntimeError};
use crate::{file_op::recursive_dir_size, file_utils};

pub async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
    if let Some(prefix) = req.prefix() {
        let config = DavConfig::new().strip_prefix(prefix);
        davhandler.handle_with(config, req.request).await.into()
    } else {
        davhandler.handle(req.request).await.into()
    }
}

pub async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
    Err(RuntimeError::RouteNotFoundError(req.path().to_string()))
}

pub async fn healthcheck() -> impl Responder {
    HttpResponse::Ok().body("OK")
}

#[derive(Deserialize, Debug)]
pub enum ApiCommand {
    /// Request the size of a particular directory
    CalculateDirSizes(Vec<String>),
}

pub type DirSizeJoinSet = JoinSet<Result<DirSize, RuntimeError>>;

// Holds the result of a calculated dir size
#[derive(Debug, Clone)]
pub struct DirSize {
    /// The web path of the dir (not the filesystem path)
    pub web_path: String,

    /// The calculcated recursive size of the dir
    pub size: u64,
}

// Reply for a calculated dir size
#[derive(Debug, Clone, Serialize)]
pub struct DirSizeReply {
    /// The web path of the dir (not the filesystem path)
    pub web_path: String,

    /// The formatted size of the dir
    pub size: String,
}

// Reply to check whether the client is still connected
//
// If the client has disconnected, we can cancel all the tasks and save some compute.
#[derive(Debug, Clone, Serialize)]
pub struct HeartbeatReply;

/// SSE API route that yields an event stream that clients can subscribe to
pub async fn api_sse(
    config: web::Data<MiniserveConfig>,
    task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
) -> impl Responder {
    let (sender, receiver) = tokio::sync::mpsc::channel(2);

    actix_web::rt::spawn(async move {
        loop {
            let msg = match task_joinset.lock().await.try_join_next() {
                Some(Ok(Ok(finished_task))) => {
                    let dir_size = if config.show_exact_bytes {
                        format!("{} B", finished_task.size)
                    } else {
                        ByteSize::b(finished_task.size).to_string()
                    };

                    let dir_size_reply = DirSizeReply {
                        web_path: finished_task.web_path,
                        size: dir_size,
                    };

                    sse::Data::new_json(dir_size_reply)
                        .expect("Couldn't serialize as JSON")
                        .event("dir-size")
                }
                Some(Ok(Err(e))) => {
                    error!("Some error during dir size calculation: {e}");
                    break;
                }
                Some(Err(e)) => {
                    error!("Some error during dir size calculation joining: {e}");
                    break;
                }
                None => sse::Data::new_json(HeartbeatReply)
                    .expect("Couldn't serialize as JSON")
                    .event("heartbeat"),
            };

            if sender.send(msg.into()).await.is_err() {
                warn!("Client disconnected; could not send SSE message");
                break;
            }

            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    });

    sse::Sse::from_infallible_receiver(receiver).with_keep_alive(Duration::from_secs(3))
}

async fn handle_dir_size_tasks(
    dirs: Vec<String>,
    config: &MiniserveConfig,
    task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
) -> Result<(), RuntimeError> {
    for dir in dirs {
        // The dir argument might be percent-encoded so let's decode it just in case.
        let decoded_path = percent_decode_str(&dir)
            .decode_utf8()
            .map_err(|e| RuntimeError::ParseError(dir.clone(), e.to_string()))?;

        // Convert the relative dir to an absolute path on the system.
        let sanitized_path =
            file_utils::sanitize_path(&*decoded_path, true).expect("Expected a path to directory");

        let full_path = config
            .path
            .canonicalize()
            .expect("Couldn't canonicalize path")
            .join(sanitized_path);
        info!("Requested directory size for {full_path:?}");

        let mut joinset = task_joinset.lock().await;
        joinset.spawn(async move {
            recursive_dir_size(&full_path).await.map(|dir_size| {
                info!("Finished dir size calculation for {full_path:?}");
                DirSize {
                    web_path: dir,
                    size: dir_size,
                }
            })
        });
    }
    Ok(())
}

/// This "API" is pretty shitty but frankly miniserve doesn't really need a very fancy API. Or at
/// least I hope so.
pub async fn api_command(
    command: web::Json<ApiCommand>,
    config: web::Data<MiniserveConfig>,
    task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
) -> Result<impl Responder, RuntimeError> {
    match command.into_inner() {
        ApiCommand::CalculateDirSizes(dirs) => {
            handle_dir_size_tasks(dirs, &config, task_joinset).await?;
            Ok("Directories are being calculated")
        }
    }
}

pub async fn favicon() -> impl Responder {
    let logo = include_str!("../data/logo.svg");
    HttpResponse::Ok()
        .insert_header(ContentType(mime::IMAGE_SVG))
        .body(logo)
}

pub async fn css(stylesheet: web::Data<String>) -> impl Responder {
    HttpResponse::Ok()
        .insert_header(ContentType(mime::TEXT_CSS))
        .body(stylesheet.to_string())
}