Skip to main content

webdav_server/storage/
error.rs

1use std::{num::TryFromIntError, path::PathBuf, time::SystemTimeError};
2
3use axum::response::IntoResponse;
4use hyper::StatusCode;
5use tokio::io;
6
7use crate::{
8    error::internal,
9    logger::{self, Loggable},
10    storage, wrap_internal_err,
11};
12
13/// Errors that can occur in the storage layer.
14///
15/// These are filesystem and transaction-level failures. Most are internal
16/// and should never be exposed to the client in detail. The `IntoResponse`
17/// implementation handles sanitization automatically.
18#[derive(thiserror::Error, Debug)]
19pub enum Error {
20    /// The vault directory does not exist on disk.
21    /// This is a fatal misconfiguration and means the server was started
22    /// with an invalid `vault_path`.
23    #[error("Root storage doesn't exist")]
24    VaultNotFound,
25
26    /// The provided filename contained illegal characters (`/`, `\`, or was empty).
27    /// This protects against path traversal attacks.
28    #[error("Invalid File Name")]
29    InvalidFileName,
30
31    /// A file with this name already exists at the target path.
32    /// This is returned when a duplicate upload is attempted for the exact same filename.
33    #[error("File Already Exists : {}", .0)]
34    FileAlreadyExists(String),
35
36    /// The server failed to create the temporary staging file before streaming begins.
37    #[error("Couldn't create temporary file at {path}")]
38    CreateTempFile {
39        path: PathBuf,
40        #[source]
41        source: io::Error,
42    },
43
44    /// A write to disk failed mid-stream while appending a chunk to the temporary file.
45    #[error("Writing chunk to disk failed, file : {path}")]
46    WriteChunkFailure {
47        path: PathBuf,
48        #[source]
49        source: io::Error,
50    },
51
52    /// Reading the next chunk from the incoming network stream failed.
53    #[error("Couldn't gather next chunk of data : {}", .0)]
54    StreamReadError(#[from] io::Error),
55
56    /// The atomic rename from the temporary staging file to the final CAS path failed.
57    #[error("Failed to rename file : {path}")]
58    RenameError {
59        path: PathBuf,
60        #[source]
61        source: io::Error,
62    },
63
64    /// The constructed path pointed outside the vault directory.
65    #[error("Invalid Path : {path}")]
66    InvalidPath { path: PathBuf },
67
68    /// A low-level internal error, typically from integer or time conversions.
69    #[error("Internal Error : {}", .0)]
70    Internal(#[from] internal::Error),
71
72    /// The requested blob was not found in the vault.
73    #[error("Blob Not found")]
74    NotFound,
75}
76
77pub type Result<T> = core::result::Result<T, storage::Error>;
78
79wrap_internal_err! { TryFromIntError, SystemTimeError => Error::Internal }
80
81impl IntoResponse for Error {
82    fn into_response(self) -> axum::response::Response {
83        use Error::{
84            CreateTempFile, FileAlreadyExists, Internal, InvalidFileName,
85            InvalidPath, NotFound, RenameError, StreamReadError, VaultNotFound,
86            WriteChunkFailure,
87        };
88
89        match self {
90            // Client-facing errors: safe to expose details.
91            InvalidFileName | InvalidPath { .. } => {
92                (StatusCode::BAD_REQUEST, "Invalid file path or name".into())
93            }
94            FileAlreadyExists(msg) => (StatusCode::CONFLICT, msg),
95            NotFound => (StatusCode::NOT_FOUND, "Blob not found".into()),
96
97            // Internal errors: hide details from the client.
98            VaultNotFound
99            | CreateTempFile { .. }
100            | WriteChunkFailure { .. }
101            | StreamReadError(_)
102            | RenameError { .. }
103            | Internal(_) => (
104                StatusCode::INTERNAL_SERVER_ERROR,
105                "Internal Server Error".into(),
106            ),
107        }
108        .into_response()
109    }
110}
111
112impl Loggable for Error {
113    fn log_level(&self) -> crate::logger::Level {
114        use logger::Level;
115        match self {
116            Self::VaultNotFound => Level::Fatal,
117            _ => Level::Error,
118        }
119    }
120
121    fn log_module(&self) -> logger::Module {
122        logger::Module::Storage
123    }
124}