nixpacks/providers/
staticfile.rs

1use super::Provider;
2use crate::nixpacks::{
3    app::{App, StaticAssets},
4    environment::Environment,
5    nix::pkg::Pkg,
6    plan::{
7        phase::{Phase, StartPhase},
8        BuildPlan,
9    },
10};
11use anyhow::Result;
12use indoc::formatdoc;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::fmt::Write as _;
16
17#[derive(Serialize, Deserialize, Default, Debug)]
18pub struct Staticfile {
19    pub root: Option<String>,
20    pub directory: Option<String>,
21    pub gzip: Option<String>,
22    pub status_code: Option<HashMap<u32, String>>,
23}
24
25pub struct StaticfileProvider {}
26
27impl Provider for StaticfileProvider {
28    fn name(&self) -> &'static str {
29        "staticfile"
30    }
31
32    fn detect(&self, app: &App, _env: &Environment) -> Result<bool> {
33        Ok(app.includes_file("Staticfile")
34            || app.includes_directory("public")
35            || app.includes_directory("index")
36            || app.includes_directory("dist")
37            || app.includes_file("index.html"))
38    }
39
40    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
41        let mut setup = Phase::setup(Some(vec![Pkg::new("nginx")]));
42        setup.add_cmd("mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/");
43
44        // shell command to edit 0.0.0.0:80 to $PORT
45        let shell_cmd = "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\"";
46        let start = StartPhase::new(format!(
47            "{shell_cmd} {conf_location} && nginx -c {conf_location}",
48            shell_cmd = shell_cmd,
49            conf_location = app.asset_path("nginx.conf"),
50        ));
51
52        let static_assets = StaticfileProvider::get_static_assets(app, env)?;
53
54        let mut plan = BuildPlan::new(&vec![setup], Some(start));
55        plan.add_static_assets(static_assets);
56
57        Ok(Some(plan))
58    }
59}
60
61impl StaticfileProvider {
62    pub fn get_root(app: &App, env: &Environment, staticfile_root: String) -> String {
63        let mut root = String::new();
64        if let Some(staticfile_root) = env.get_config_variable("STATICFILE_ROOT") {
65            root = staticfile_root;
66        } else if !staticfile_root.is_empty() {
67            root = staticfile_root;
68        } else if app.includes_directory("public") {
69            root = "public".to_string();
70        } else if app.includes_directory("dist") {
71            root = "dist".to_string();
72        } else if app.includes_directory("index") {
73            root = "index".to_string();
74        }
75
76        root
77    }
78
79    fn get_static_assets(app: &App, env: &Environment) -> Result<StaticAssets> {
80        let mut assets = StaticAssets::new();
81
82        let mut mime_types = "include /nix/store/*-user-environment/conf/mime.types;".to_string();
83        if app.includes_file("mime.types") {
84            assets.insert("mime.types".to_string(), app.read_file("mime.types")?);
85            mime_types = "include\tmime.types;".to_string();
86        }
87
88        let mut auth_basic = String::new();
89        if app.includes_file("Staticfile.auth") {
90            assets.insert(".htpasswd".to_string(), app.read_file("Staticfile.auth")?);
91            auth_basic = format!(
92                "auth_basic\t\"Password Required\";\nauth_basic_user_file\t{};",
93                app.asset_path(".htpasswd")
94            );
95        }
96
97        let staticfile: Staticfile = app.read_yaml("Staticfile").unwrap_or_default();
98        let root = StaticfileProvider::get_root(app, env, staticfile.root.unwrap_or_default());
99        let gzip = staticfile.gzip.unwrap_or_else(|| "on".to_string());
100        let directory = staticfile.directory.unwrap_or_else(|| "off".to_string());
101        let status_code = staticfile.status_code.unwrap_or_default();
102        let mut error_page = String::new();
103        for (key, value) in status_code {
104            writeln!(error_page, "\terror_page {key} {value};")?;
105        }
106
107        let nginx_conf = formatdoc! {"
108        daemon off;
109        error_log /dev/stdout info;
110        worker_processes  auto;
111        events {{
112            worker_connections  1024;
113        }}
114        
115        http {{
116            {mime_types}
117            access_log /dev/stdout;
118            default_type  application/octet-stream;
119            sendfile       on;
120            keepalive_timeout  60;
121            types_hash_max_size 4096;
122            server {{
123                listen    0.0.0.0:80;
124                gzip  	  {gzip};
125                root	  /app/{root};
126                location / {{
127                    {auth_basic}
128                    autoindex {directory};
129                }}
130        {error_page}
131            }}
132        }}
133        ", 
134        mime_types = mime_types,
135        gzip = gzip,
136        root = root,
137        auth_basic = auth_basic,
138        directory = directory,
139        error_page = error_page
140        };
141        assets.insert("nginx.conf".to_string(), nginx_conf);
142
143        Ok(assets)
144    }
145}