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
use std::path::Path;

use reqwest::blocking::Client;
use serde::Deserialize;

use crate::http;
use crate::kv::bulk;
use crate::settings::global_user::GlobalUser;
use crate::settings::toml::Target;
use crate::sites::{add_namespace, sync, AssetManifest};
use crate::terminal::message::{Message, StdOut};
use crate::terminal::styles;
use crate::upload;

#[derive(Debug, Deserialize)]
struct Preview {
    id: String,
}

impl From<ApiPreview> for Preview {
    fn from(api_preview: ApiPreview) -> Preview {
        Preview {
            id: api_preview.preview_id,
        }
    }
}

// When making authenticated preview requests, we go through the v4 Workers API rather than
// hitting the preview service directly, so its response is formatted like a v4 API response.
// These structs are here to convert from this format into the Preview defined above.
#[derive(Debug, Deserialize)]
struct ApiPreview {
    pub preview_id: String,
}

#[derive(Debug, Deserialize)]
struct V4ApiResponse {
    pub result: ApiPreview,
}

const SITES_UNAUTH_PREVIEW_ERR: &str =
    "Unauthenticated preview does not work for previewing Workers Sites; you need to \
     authenticate to upload your site contents.";

// Builds and uploads the script and its bindings. Returns the ID of the uploaded script.
pub fn upload(
    target: &mut Target,
    user: Option<&GlobalUser>,
    sites_preview: bool,
    verbose: bool,
) -> Result<String, failure::Error> {
    let preview = match &user {
        Some(user) => {
            log::info!("GlobalUser set, running with authentication");

            let missing_fields = validate(&target);

            if missing_fields.is_empty() {
                let client = http::legacy_auth_client(&user);

                if let Some(site_config) = target.site.clone() {
                    let site_namespace = add_namespace(user, target, true)?;

                    let path = Path::new(&site_config.bucket);
                    let (to_upload, to_delete, asset_manifest) =
                        sync(target, user, &site_namespace.id, path)?;

                    // First, upload all existing files in given directory
                    if verbose {
                        StdOut::info("Uploading updated files...");
                    }

                    bulk::put(target, user, &site_namespace.id, to_upload, &None)?;

                    let preview = authenticated_upload(&client, &target, Some(asset_manifest))?;
                    if !to_delete.is_empty() {
                        if verbose {
                            StdOut::info("Deleting stale files...");
                        }

                        bulk::delete(target, user, &site_namespace.id, to_delete, &None)?;
                    }

                    preview
                } else {
                    authenticated_upload(&client, &target, None)?
                }
            } else {
                StdOut::warn(&format!(
                    "Your configuration file is missing the following fields: {:?}",
                    missing_fields
                ));
                StdOut::warn("Falling back to unauthenticated preview.");
                if sites_preview {
                    failure::bail!(SITES_UNAUTH_PREVIEW_ERR)
                }

                unauthenticated_upload(&target)?
            }
        }
        None => {
            let wrangler_config_msg = styles::highlight("`wrangler config`");
            let wrangler_login_msg = styles::highlight("`wrangler login`");
            let docs_url_msg = styles::url("https://developers.cloudflare.com/workers/tooling/wrangler/configuration/#using-environment-variables");
            StdOut::billboard(
            &format!("You have not provided your Cloudflare credentials.\n\nPlease run {}, {}, or visit\n{}\nfor info on authenticating with environment variables.", wrangler_login_msg, wrangler_config_msg, docs_url_msg)
            );

            StdOut::info("Running preview without authentication.");

            if sites_preview {
                failure::bail!(SITES_UNAUTH_PREVIEW_ERR)
            }

            unauthenticated_upload(&target)?
        }
    };

    Ok(preview.id)
}

fn validate(target: &Target) -> Vec<&str> {
    let mut missing_fields = Vec::new();

    if target.account_id.is_empty() {
        missing_fields.push("account_id")
    };
    if target.name.is_empty() {
        missing_fields.push("name")
    };

    for kv in &target.kv_namespaces {
        if kv.binding.is_empty() {
            missing_fields.push("kv-namespace binding")
        }

        if kv.id.is_empty() {
            missing_fields.push("kv-namespace id")
        }
    }

    missing_fields
}

fn authenticated_upload(
    client: &Client,
    target: &Target,
    asset_manifest: Option<AssetManifest>,
) -> Result<Preview, failure::Error> {
    let create_address = format!(
        "https://api.cloudflare.com/client/v4/accounts/{}/workers/scripts/{}/preview",
        target.account_id, target.name
    );
    log::info!("address: {}", create_address);

    let script_upload_form = upload::form::build(target, asset_manifest, None)?;

    let res = client
        .post(&create_address)
        .multipart(script_upload_form)
        .send()?;

    if !res.status().is_success() {
        failure::bail!(
            "Something went wrong! Status: {}, Details {}",
            res.status(),
            res.text()?
        )
    }

    let text = &res.text()?;
    log::info!("Response from preview: {:#?}", text);

    let response: V4ApiResponse =
        serde_json::from_str(text).expect("could not create a script on cloudflareworkers.com");

    Ok(Preview::from(response.result))
}

fn unauthenticated_upload(target: &Target) -> Result<Preview, failure::Error> {
    let create_address = "https://cloudflareworkers.com/script";
    log::info!("address: {}", create_address);

    let mut target = target.clone();
    // KV namespaces and sites are not supported by the preview service unless you authenticate
    // so we omit them and provide the user with a little guidance. We don't error out, though,
    // because there are valid workarounds for this for testing purposes.
    if !target.kv_namespaces.is_empty() {
        StdOut::warn(
            "KV Namespaces are not supported in preview without setting API credentials and account_id",
        );

        target.kv_namespaces = Vec::new();
    }
    if target.site.is_some() {
        StdOut::warn(
            "Sites are not supported in preview without setting API credentials and account_id",
        );
        target.site = None;
    }

    let script_upload_form = upload::form::build(&target, None, None)?;
    let client = http::client();
    let res = client
        .post(create_address)
        .multipart(script_upload_form)
        .send()?;

    if !res.status().is_success() {
        failure::bail!(
            "Something went wrong! Status: {}, Details {}",
            res.status(),
            res.text()?
        )
    }

    let text = &res.text()?;
    log::info!("Response from preview: {:#?}", text);

    let preview: Preview =
        serde_json::from_str(text).expect("could not create a script on cloudflareworkers.com");

    Ok(preview)
}