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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! Parse CLI options, setup on boot, read .env values

use crate::errors::AtomicServerResult;
use clap::Parser;
use dotenv::dotenv;
use std::net::IpAddr;
use std::path::PathBuf;

/// Store and share Atomic Data! Visit https://atomicdata.dev for more info. Pass no subcommands to launch the server. The `.env` of your current directory will be read.
#[derive(Clone, Parser, Debug)]
#[clap(about, author, version)]
pub struct Opts {
    /// The subcommand being run
    #[clap(subcommand)]
    pub command: Option<Command>,

    /// Recreates the `/setup` Invite for creating a new Root User. Also re-runs various populate commands, and re-builds the index
    #[clap(long, env = "ATOMIC_INITIALIZE")]
    pub initialize: bool,

    /// Re-builds the indexes. Parses all the resources.
    /// Do this when updating requires it, or if you have issues with Collections / Queries / Search.
    #[clap(long, env = "ATOMIC_REBUILD_INDEX")]
    pub rebuild_indexes: bool,

    /// Use staging environments for services like LetsEncrypt
    #[clap(long, env = "ATOMIC_DEVELOPMENT")]
    pub development: bool,

    /// The origin domain where the app is hosted, without the port and schema values.
    #[clap(long, default_value = "localhost", env = "ATOMIC_DOMAIN")]
    pub domain: String,

    // 9.883 is decimal for the `⚛` character.
    /// The port where the HTTP app is available. Set to 80 if you want this to be available on the network.
    #[clap(short, long, default_value = "9883", env = "ATOMIC_PORT")]
    pub port: u32,

    /// The port where the HTTPS app is available. Set to 443 if you want this to be available on the network.
    #[clap(
        long,
        default_value = "9884",
        env = "ATOMIC_PORT_HTTPS",
        requires = "https"
    )]
    pub port_https: u32,

    /// The IP address of the server. Set to :: if you want this to be available to other devices on your network.
    #[clap(long, default_value = "::", env = "ATOMIC_IP")]
    pub ip: IpAddr,

    /// Use HTTPS instead of HTTP.
    /// Will get certificates from LetsEncrypt fully automated.
    #[clap(long, env = "ATOMIC_HTTPS")]
    pub https: bool,

    /// Initializes DNS-01 challenge for LetsEncrypt. Use this if you want to use subdomains.
    #[clap(long, env = "ATOMIC_HTTPS_DNS", requires = "https")]
    pub https_dns: bool,

    /// The contact mail address for Let's Encrypt HTTPS setup
    #[clap(long, env = "ATOMIC_EMAIL")]
    pub email: Option<String>,

    /// Custom JS script to include in the body of the HTML template
    #[clap(long, default_value = "", env = "ATOMIC_SCRIPT")]
    pub script: String,

    /// Path for atomic data config directory. Defaults to "~/.config/atomic/""
    #[clap(long, env = "ATOMIC_CONFIG_DIR")]
    pub config_dir: Option<PathBuf>,

    /// Path for atomic data store folder. Contains your Store, uploaded files and more. Default value depends on your OS.
    #[clap(long, env = "ATOMIC_DATA_DIR")]
    pub data_dir: Option<PathBuf>,

    /// CAUTION: Skip authentication checks, making all data publicly readable. Improves performance.
    #[clap(long, env = "ATOMIC_PUBLIC_MODE")]
    pub public_mode: bool,

    /// The full URL of the server. It should resolve to the home page. Set this if you use an external server or tunnel, instead of directly exposing atomic-server. If you leave this out, it will be generated from `domain`, `port` and `http` / `https`.
    #[clap(long, env = "ATOMIC_SERVER_URL")]
    pub server_url: Option<String>,

    /// How much logs you want. Also influences what is sent to your trace service, if you've set one (e.g. OpenTelemetry)
    #[clap(value_enum, long, default_value = "info", env = "RUST_LOG")]
    pub log_level: LogLevel,

    /// How you want to trace what's going on with the server. Useful for monitoring performance and errors in production.
    /// Combine with `log_level` to get more or less data (`trace` is the most verbose)
    #[clap(value_enum, long, env = "ATOMIC_TRACING", default_value = "stdout")]
    pub trace: Tracing,

    /// Introduces random delays in the server, to simulate a slow connection. Useful for testing.
    #[clap(long, env = "ATOMIC_SLOW_MODE")]
    pub slow_mode: bool,
}

#[derive(clap::ValueEnum, Clone, Debug)]
pub enum Tracing {
    /// Log to STDOUT in your terminal
    Stdout,
    /// Create a file in the current directory with tracing data, that can be opened with the chrome://tracing/ URL
    Chrome,
    /// Log to a local OpenTelemetry service (e.g. Jaeger), using default ports
    Opentelemetry,
}

#[derive(clap::ValueEnum, Clone, Debug)]
pub enum LogLevel {
    Warn,
    Info,
    Debug,
    Trace,
}

#[derive(Parser, Clone, Debug)]
pub enum Command {
    /// Create and save a JSON-AD backup of the store.
    #[clap(name = "export")]
    Export(ExportOpts),
    /// Import a JSON-AD file or stream to the store. By default creates Commits for all changes, maintaining version history. Use --force to allow importing other types of files.
    #[clap(name = "import", trailing_var_arg = true)]
    Import(ImportOpts),
    /// Creates a `.env` file in your current directory that shows various options that you can set.
    #[clap(name = "generate-dotenv")]
    CreateDotEnv,
    /// Returns the currently selected options, based on the passed flags and parsed environment variables.
    #[clap(name = "show-config")]
    ShowConfig,
    /// Danger! Removes all data from the store.
    #[clap(name = "reset")]
    Reset,
}

#[derive(Parser, Clone, Debug)]
pub struct ExportOpts {
    /// Where the exported file should be saved  "~/.config/atomic/backups/{date}.json"
    #[clap(short)]
    pub path: Option<PathBuf>,
    /// Do not export resources that are externally defined, which are cached by this Server.
    #[clap(long)]
    pub only_internal: bool,
}

#[derive(Parser, Clone, Debug)]
pub struct ImportOpts {
    /// Path of the file to be imported.
    #[clap(long)]
    pub file: PathBuf,
    /// The URL of the  Importer (parent) Resource to be used.
    /// This will set the hierarchical location of the imported items.
    /// If not passed, the default Importer `/import` will be used.
    #[clap(long)]
    pub parent: Option<String>,
    /// Skip checks, allows for importing things like Commits.
    #[clap(long)]
    pub force: bool,
}

/// Start atomic-server, oi mate
#[derive(Parser, Clone, Debug)]
pub struct ServerOpts {}

/// Configuration for the server.
/// These values are set when the server initializes, and do not change while running.
/// These are constructed from [Opts], which in turn are constructed from CLI arguments and ENV variables.
#[derive(Clone, Debug)]
pub struct Config {
    /// Full domain + schema, e.g. `https://example.com`. Is either generated from `domain` and `schema`, or is the `custom_server_url`.
    pub server_url: String,
    /// CLI + ENV options
    pub opts: Opts,
    // ===  PATHS  ===
    /// Path for atomic data config. Used to construct most other paths.
    pub config_dir: PathBuf,
    /// Path where TLS key should be stored for HTTPS.
    pub key_path: PathBuf,
    /// Path where TLS certificate should be stored for HTTPS.
    pub cert_path: PathBuf,
    /// Path where TLS certificates should be stored for HTTPS.
    pub https_path: PathBuf,
    /// Path where config.toml is located, which contains info about the Agent
    pub config_file_path: PathBuf,
    /// Path where the public static files folder is located
    pub static_path: PathBuf,
    /// Path to where the store / database is located.
    pub store_path: PathBuf,
    /// Path to where the uploaded files are stored.
    pub uploads_path: PathBuf,
    /// Path to where the search index for tantivy full text search is located
    pub search_index_path: PathBuf,
    /// If true, the initialization scripts will be ran (create first Drive, Agent, indexing, etc)
    pub initialize: bool,
}

/// Parse .env and CLI options
pub fn read_opts() -> Opts {
    // Parse .env file (do this before parsing the CLI opts)
    dotenv().ok();

    // Parse CLI options, .env values, set defaults
    Opts::parse()
}

/// Creates the server config, reads .env values and sets defaults
pub fn build_config(opts: Opts) -> AtomicServerResult<Config> {
    // Directories & file system
    let project_dirs = directories::ProjectDirs::from("", "", "atomic-data")
        .expect("Could not find Project directories on your OS");

    // Persistent user data
    let data_dir = opts
        .data_dir
        .clone()
        .unwrap_or_else(|| project_dirs.data_dir().to_owned());
    let mut store_path = data_dir.clone();
    store_path.push("store");

    let mut uploads_path = data_dir.clone();
    uploads_path.push("uploads");

    let mut static_path = data_dir;
    static_path.push("static");

    // Config data
    let config_dir = if let Some(dir) = &opts.config_dir {
        dir.clone()
    } else {
        atomic_lib::config::default_config_dir_path()?
    };
    let config_file_path = config_dir.join("config.toml");

    let mut https_path = config_dir.clone();
    https_path.push("https");

    let mut cert_path = config_dir.clone();
    cert_path.push("https/cert.pem");

    let mut key_path = config_dir.clone();
    key_path.push("https/key.pem");

    // Cache data

    let cache_dir = project_dirs.cache_dir();

    let mut search_index_path = cache_dir.to_owned();
    search_index_path.push("search_index");

    let initialize = !std::path::Path::exists(&store_path) || opts.initialize;

    if opts.https & opts.email.is_none() {
        return Err(
            "The `--email` flag (or ATOMIC_EMAIL env) is required for getting an HTTPS certificate from letsencrypt.org."
                .into(),
        );
    }

    let schema = if opts.https { "https" } else { "http" };

    // This logic could be a bit too complicated, but I'm not sure on how to make this simpler.
    let server_url = if let Some(addr) = opts.server_url.clone() {
        addr
    } else if opts.https && opts.port_https == 443 || !opts.https && opts.port == 80 {
        format!("{}://{}", schema, opts.domain)
    } else {
        format!("{}://{}:{}", schema, opts.domain, opts.port)
    };

    Ok(Config {
        initialize,
        opts,
        cert_path,
        config_dir,
        config_file_path,
        https_path,
        key_path,
        server_url,
        static_path,
        store_path,
        search_index_path,
        uploads_path,
    })
}