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
use super::Provider;
use crate::nixpacks::{
    app::{App, StaticAssets},
    environment::Environment,
    nix::pkg::Pkg,
    plan::{
        phase::{Phase, StartPhase},
        BuildPlan,
    },
};
use anyhow::Result;
use indoc::formatdoc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write as _;

#[derive(Serialize, Deserialize, Default, Debug)]
pub struct Staticfile {
    pub root: Option<String>,
    pub directory: Option<String>,
    pub gzip: Option<String>,
    pub status_code: Option<HashMap<u32, String>>,
}

pub struct StaticfileProvider {}

impl Provider for StaticfileProvider {
    fn name(&self) -> &str {
        "staticfile"
    }

    fn detect(&self, app: &App, _env: &Environment) -> Result<bool> {
        Ok(app.includes_file("Staticfile")
            || app.includes_directory("public")
            || app.includes_directory("index")
            || app.includes_directory("dist")
            || app.includes_file("index.html"))
    }

    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
        let mut setup = Phase::setup(Some(vec![Pkg::new("nginx")]));
        setup.add_cmd("mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/");

        // shell command to edit 0.0.0.0:80 to $PORT
        let shell_cmd = "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\"";
        let start = StartPhase::new(format!(
            "{shell_cmd} {conf_location} && nginx -c {conf_location}",
            shell_cmd = shell_cmd,
            conf_location = app.asset_path("nginx.conf"),
        ));

        let static_assets = StaticfileProvider::get_static_assets(app, env)?;

        let mut plan = BuildPlan::new(&vec![setup], Some(start));
        plan.add_static_assets(static_assets);

        Ok(Some(plan))
    }
}

impl StaticfileProvider {
    pub fn get_root(app: &App, env: &Environment, staticfile_root: String) -> String {
        let mut root = String::new();
        if let Some(staticfile_root) = env.get_config_variable("STATICFILE_ROOT") {
            root = staticfile_root;
        } else if !staticfile_root.is_empty() {
            root = staticfile_root;
        } else if app.includes_directory("public") {
            root = "public".to_string();
        } else if app.includes_directory("dist") {
            root = "dist".to_string();
        } else if app.includes_directory("index") {
            root = "index".to_string();
        }

        root
    }

    fn get_static_assets(app: &App, env: &Environment) -> Result<StaticAssets> {
        let mut assets = StaticAssets::new();

        let mut mime_types = "include /nix/store/*-user-environment/conf/mime.types;".to_string();
        if app.includes_file("mime.types") {
            assets.insert("mime.types".to_string(), app.read_file("mime.types")?);
            mime_types = "include\tmime.types;".to_string();
        }

        let mut auth_basic = String::new();
        if app.includes_file("Staticfile.auth") {
            assets.insert(".htpasswd".to_string(), app.read_file("Staticfile.auth")?);
            auth_basic = format!(
                "auth_basic\t\"Password Required\";\nauth_basic_user_file\t{};",
                app.asset_path(".htpasswd")
            );
        }

        let staticfile: Staticfile = app.read_yaml("Staticfile").unwrap_or_default();
        let root = StaticfileProvider::get_root(app, env, staticfile.root.unwrap_or_default());
        let gzip = staticfile.gzip.unwrap_or_else(|| "on".to_string());
        let directory = staticfile.directory.unwrap_or_else(|| "off".to_string());
        let status_code = staticfile.status_code.unwrap_or_default();
        let mut error_page = String::new();
        for (key, value) in status_code {
            writeln!(error_page, "\terror_page {key} {value};")?;
        }

        let nginx_conf = formatdoc! {"
        daemon off;
        error_log /dev/stdout info;
        worker_processes  auto;
        events {{
            worker_connections  1024;
        }}
        
        http {{
            {mime_types}
            access_log /dev/stdout;
            default_type  application/octet-stream;
            sendfile       on;
            keepalive_timeout  60;
            types_hash_max_size 4096;
            server {{
                listen    0.0.0.0:80;
                gzip  	  {gzip};
                root	  /app/{root};
                location / {{
                    {auth_basic}
                    autoindex {directory};
                }}
        {error_page}
            }}
        }}
        ", 
        mime_types = mime_types,
        gzip = gzip,
        root = root,
        auth_basic = auth_basic,
        directory = directory,
        error_page = error_page
        };
        assets.insert("nginx.conf".to_string(), nginx_conf);

        Ok(assets)
    }
}