mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add .env to .gitignore and update dependencies
- Added .env file to .gitignore to prevent environment secrets from being tracked. - Updated Cargo.lock to include new dependencies: mime, mime_guess, and unicase. - Modified Cargo.toml to enable multipart feature for reqwest. - Introduced a new PowerShell script for building and publishing the Rojo plugin. - Removed deprecated auth_cookie.rs file and adjusted upload command for new upload methods.
This commit is contained in:
parent
7b73720143
commit
2803c2d73a
7 changed files with 274 additions and 68 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -26,3 +26,6 @@
|
||||||
|
|
||||||
# JetBrains IDEs
|
# JetBrains IDEs
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
||||||
|
# Environment secrets
|
||||||
|
.env
|
||||||
|
|
|
||||||
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -1810,6 +1810,22 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -2545,6 +2561,7 @@ dependencies = [
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
|
|
@ -3617,6 +3634,12 @@ version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ rbx_cookie = "0.1.5"
|
||||||
reqwest = { version = "0.13", default-features = false, features = [
|
reqwest = { version = "0.13", default-features = false, features = [
|
||||||
"blocking",
|
"blocking",
|
||||||
"json",
|
"json",
|
||||||
|
"multipart",
|
||||||
"rustls",
|
"rustls",
|
||||||
"gzip",
|
"gzip",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
|
|
||||||
37
scripts/publish-plugin.ps1
Normal file
37
scripts/publish-plugin.ps1
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Build and publish the Rojo plugin to the Creator Store
|
||||||
|
# Loads API key from .env file, ROBLOX_API_KEY env var, or -ApiKey argument.
|
||||||
|
# Get an API key at https://create.roblox.com/credentials (assets read+write).
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Load .env file if it exists
|
||||||
|
$envFile = Join-Path $PSScriptRoot "..\.env"
|
||||||
|
if (Test-Path $envFile) {
|
||||||
|
foreach ($line in Get-Content $envFile) {
|
||||||
|
$line = $line.Trim()
|
||||||
|
if ($line -and -not $line.StartsWith("#")) {
|
||||||
|
$key, $value = $line -split "=", 2
|
||||||
|
if ($key -and $value) {
|
||||||
|
[Environment]::SetEnvironmentVariable($key.Trim(), $value.Trim(), "Process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve API key: argument > env var > .env (already loaded above)
|
||||||
|
if (-not $ApiKey) {
|
||||||
|
$ApiKey = $env:ROBLOX_API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ApiKey -or $ApiKey -eq "your-key-here") {
|
||||||
|
Write-Host "Error: No API key provided." -ForegroundColor Red
|
||||||
|
Write-Host "Set ROBLOX_API_KEY in .env, env var, or pass -ApiKey <key>" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Building and uploading plugin..." -ForegroundColor Cyan
|
||||||
|
cargo run -- upload plugin.project.json --asset_id 111151139098227
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
//! Implementation of automatically fetching authentication cookie from a Roblox
|
|
||||||
//! Studio installation.
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn get_auth_cookie() -> Option<String> {
|
|
||||||
use winreg::{enums::HKEY_CURRENT_USER, RegKey};
|
|
||||||
|
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
||||||
let cookies = hkcu
|
|
||||||
.open_subkey("Software\\Roblox\\RobloxStudioBrowser\\roblox.com")
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let entry: String = cookies.get_value(".ROBLOSECURITY").ok()?;
|
|
||||||
let mut cookie = None;
|
|
||||||
|
|
||||||
for kv_pair in entry.split(',') {
|
|
||||||
let mut pieces = kv_pair.split("::");
|
|
||||||
|
|
||||||
if let Some("COOK") = pieces.next() {
|
|
||||||
let value = match pieces.next() {
|
|
||||||
Some(value) => value,
|
|
||||||
None => {
|
|
||||||
log::warn!("Unrecognized Roblox Studio cookie value: missing COOK value");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !value.starts_with('<') || !value.ends_with('>') {
|
|
||||||
log::warn!("Unrecognized Roblox Studio cookie value: was not wrapped in <>");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = &value[1..value.len() - 1];
|
|
||||||
|
|
||||||
cookie = Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
pub fn get_auth_cookie() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
@ -4,30 +4,43 @@ use anyhow::{bail, Context};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
|
blocking::multipart,
|
||||||
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
|
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
};
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::serve_session::ServeSession;
|
use crate::serve_session::ServeSession;
|
||||||
|
|
||||||
use super::resolve_path;
|
use super::resolve_path;
|
||||||
|
|
||||||
|
const ASSETS_API_BASE: &str = "https://apis.roblox.com/assets/v1";
|
||||||
|
const MAX_OPERATION_RETRIES: u32 = 10;
|
||||||
|
|
||||||
/// Builds the project and uploads it to Roblox.
|
/// Builds the project and uploads it to Roblox.
|
||||||
|
///
|
||||||
|
/// Supports three upload modes:
|
||||||
|
/// - Legacy cookie auth (default): uses Data/Upload.ashx
|
||||||
|
/// - Open Cloud Assets API (--api_key only): for Creator Store assets (models, plugins)
|
||||||
|
/// - Open Cloud Places API (--api_key + --universe_id): for places
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct UploadCommand {
|
pub struct UploadCommand {
|
||||||
/// Path to the project to upload. Defaults to the current directory.
|
/// Path to the project to upload. Defaults to the current directory.
|
||||||
#[clap(default_value = "")]
|
#[clap(default_value = "")]
|
||||||
pub project: PathBuf,
|
pub project: PathBuf,
|
||||||
|
|
||||||
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
|
/// Authentication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub cookie: Option<String>,
|
pub cookie: Option<String>,
|
||||||
|
|
||||||
/// API key obtained from create.roblox.com/credentials. Rojo will use the Open Cloud API when this is provided. Only supports uploading to a place.
|
/// API key obtained from create.roblox.com/credentials.
|
||||||
|
/// Without --universe_id, uses the Open Cloud Assets API (models, plugins).
|
||||||
|
/// With --universe_id, uses the Open Cloud Places API.
|
||||||
#[clap(long = "api_key")]
|
#[clap(long = "api_key")]
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
/// The Universe ID of the given place. Required when using the Open Cloud API.
|
/// The Universe ID of the given place. When provided with --api_key,
|
||||||
|
/// uses the Open Cloud Places API instead of the Assets API.
|
||||||
#[clap(long = "universe_id")]
|
#[clap(long = "universe_id")]
|
||||||
pub universe_id: Option<u64>,
|
pub universe_id: Option<u64>,
|
||||||
|
|
||||||
|
|
@ -60,7 +73,7 @@ impl UploadCommand {
|
||||||
|
|
||||||
match (self.cookie, self.api_key, self.universe_id) {
|
match (self.cookie, self.api_key, self.universe_id) {
|
||||||
(cookie, None, universe) => {
|
(cookie, None, universe) => {
|
||||||
// using legacy. notify if universe is provided.
|
// Legacy cookie auth
|
||||||
if universe.is_some() {
|
if universe.is_some() {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"--universe_id was provided but is ignored when using legacy upload"
|
"--universe_id was provided but is ignored when using legacy upload"
|
||||||
|
|
@ -72,27 +85,32 @@ impl UploadCommand {
|
||||||
.context(
|
.context(
|
||||||
"Rojo could not find your Roblox auth cookie. Please log into Roblox Studio or pass one via --cookie.",
|
"Rojo could not find your Roblox auth cookie. Please log into Roblox Studio or pass one via --cookie.",
|
||||||
)?;
|
)?;
|
||||||
do_upload(buffer, self.asset_id, &cookie)
|
do_upload_legacy(buffer, self.asset_id, &cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
(cookie, Some(api_key), Some(universe_id)) => {
|
(cookie, Some(api_key), None) => {
|
||||||
// using open cloud. notify if cookie is provided.
|
// Open Cloud Assets API (models, plugins, Creator Store assets)
|
||||||
if cookie.is_some() {
|
if cookie.is_some() {
|
||||||
log::warn!("--cookie was provided but is ignored when using Open Cloud API");
|
log::warn!("--cookie was provided but is ignored when using Open Cloud API");
|
||||||
}
|
}
|
||||||
|
|
||||||
do_upload_open_cloud(buffer, universe_id, self.asset_id, &api_key)
|
do_upload_asset(buffer, self.asset_id, &api_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
(_, Some(_), None) => {
|
(cookie, Some(api_key), Some(universe_id)) => {
|
||||||
// API key is provided, universe id is not.
|
// Open Cloud Places API
|
||||||
bail!("--universe_id must be provided to use the Open Cloud API");
|
if cookie.is_some() {
|
||||||
|
log::warn!("--cookie was provided but is ignored when using Open Cloud API");
|
||||||
|
}
|
||||||
|
|
||||||
|
do_upload_place(buffer, universe_id, self.asset_id, &api_key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
|
/// Legacy upload via Data/Upload.ashx with cookie auth.
|
||||||
|
fn do_upload_legacy(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
||||||
asset_id
|
asset_id
|
||||||
|
|
@ -110,7 +128,7 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
|
||||||
.body(buffer.clone())
|
.body(buffer.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("Uploading to Roblox...");
|
log::debug!("Uploading to Roblox (legacy)...");
|
||||||
let mut response = build_request().send()?;
|
let mut response = build_request().send()?;
|
||||||
|
|
||||||
// Starting in Feburary, 2021, the upload endpoint performs CSRF challenges.
|
// Starting in Feburary, 2021, the upload endpoint performs CSRF challenges.
|
||||||
|
|
@ -125,18 +143,182 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
|
let body = response.text()?;
|
||||||
bail!(
|
bail!(
|
||||||
"The Roblox API returned an unexpected error: {}",
|
"The Roblox API returned HTTP {}: {}",
|
||||||
response.text()?
|
status,
|
||||||
|
if body.is_empty() {
|
||||||
|
"(empty response)".to_owned()
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of do_upload that supports the new open cloud api.
|
/// Upload via the Open Cloud Assets API (PATCH /assets/v1/assets/{assetId}).
|
||||||
/// see https://developer.roblox.com/en-us/articles/open-cloud
|
/// Works for Creator Store assets: models, plugins, etc.
|
||||||
fn do_upload_open_cloud(
|
///
|
||||||
|
/// Follows the same flow as Roblox's rbxasset tool:
|
||||||
|
/// 1. PATCH the asset with multipart form (request JSON + file content)
|
||||||
|
/// 2. Poll the returned operation until completion
|
||||||
|
///
|
||||||
|
/// See https://create.roblox.com/docs/reference/cloud/assets/v1
|
||||||
|
fn do_upload_asset(buffer: Vec<u8>, asset_id: u64, api_key: &str) -> anyhow::Result<()> {
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
|
// Step 1: PATCH the asset with new content
|
||||||
|
let url = format!("{}/assets/{}", ASSETS_API_BASE, asset_id);
|
||||||
|
|
||||||
|
let request_json = serde_json::json!({
|
||||||
|
"assetId": asset_id,
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let form = multipart::Form::new()
|
||||||
|
.part(
|
||||||
|
"request",
|
||||||
|
multipart::Part::text(request_json)
|
||||||
|
.mime_str("application/json")?,
|
||||||
|
)
|
||||||
|
.part(
|
||||||
|
"fileContent",
|
||||||
|
multipart::Part::bytes(buffer)
|
||||||
|
.file_name("asset.rbxm")
|
||||||
|
.mime_str("model/x-rbxm")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("Uploading to Roblox (Open Cloud Assets API)...");
|
||||||
|
let response = client
|
||||||
|
.patch(&url)
|
||||||
|
.header("x-api-key", api_key)
|
||||||
|
.multipart(form)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let body: Value = response
|
||||||
|
.json()
|
||||||
|
.context("Failed to parse upload response as JSON")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let message = body["message"].as_str().unwrap_or("unknown error");
|
||||||
|
let code = body["code"].as_str().unwrap_or("");
|
||||||
|
bail!(
|
||||||
|
"The Roblox API returned HTTP {} ({}): {}",
|
||||||
|
status,
|
||||||
|
code,
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Poll the operation until it completes
|
||||||
|
// The PATCH response returns { "path": "operations/{operationId}", "done": bool, ... }
|
||||||
|
if let Some(true) = body["done"].as_bool() {
|
||||||
|
// Operation completed immediately
|
||||||
|
log_asset_result(&body);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let operation_path = body["path"]
|
||||||
|
.as_str()
|
||||||
|
.context("Upload response missing 'path' field for operation polling")?;
|
||||||
|
|
||||||
|
// Extract operationId from "operations/{operationId}"
|
||||||
|
let operation_id = operation_path
|
||||||
|
.strip_prefix("operations/")
|
||||||
|
.unwrap_or(operation_path);
|
||||||
|
|
||||||
|
log::info!("Upload accepted, waiting for processing (operation: {})...", operation_id);
|
||||||
|
|
||||||
|
let mut retry_delay = std::time::Duration::from_secs(1);
|
||||||
|
|
||||||
|
for attempt in 1..=MAX_OPERATION_RETRIES {
|
||||||
|
std::thread::sleep(retry_delay);
|
||||||
|
|
||||||
|
let op_url = format!("{}/operations/{}", ASSETS_API_BASE, operation_id);
|
||||||
|
let op_response = client
|
||||||
|
.get(&op_url)
|
||||||
|
.header("x-api-key", api_key)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let op_status = op_response.status();
|
||||||
|
let op_body: Value = op_response
|
||||||
|
.json()
|
||||||
|
.context("Failed to parse operation response as JSON")?;
|
||||||
|
|
||||||
|
if !op_status.is_success() {
|
||||||
|
log::warn!(
|
||||||
|
"Operation poll attempt {}/{} returned HTTP {}: {}",
|
||||||
|
attempt,
|
||||||
|
MAX_OPERATION_RETRIES,
|
||||||
|
op_status,
|
||||||
|
op_body
|
||||||
|
);
|
||||||
|
retry_delay *= 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(true) = op_body["done"].as_bool() {
|
||||||
|
// Check for error in the operation result
|
||||||
|
if op_body.get("error").is_some() {
|
||||||
|
let err_message = op_body["error"]["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error");
|
||||||
|
bail!("Asset processing failed: {}", err_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_asset_result(&op_body);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Operation not yet complete (attempt {}/{}), retrying in {}s...",
|
||||||
|
attempt,
|
||||||
|
MAX_OPERATION_RETRIES,
|
||||||
|
retry_delay.as_secs()
|
||||||
|
);
|
||||||
|
|
||||||
|
retry_delay *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!(
|
||||||
|
"Operation {} did not complete within {} attempts",
|
||||||
|
operation_id,
|
||||||
|
MAX_OPERATION_RETRIES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_asset_result(body: &Value) {
|
||||||
|
if let Some(response) = body.get("response") {
|
||||||
|
let asset_id = response
|
||||||
|
.get("assetId")
|
||||||
|
.and_then(|v| v.as_str().or_else(|| v.as_u64().map(|_| "")));
|
||||||
|
let revision = response.get("revisionId").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
match (asset_id, revision) {
|
||||||
|
(Some(_), Some(rev)) => {
|
||||||
|
log::info!("Upload complete! Revision: {}", rev);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::info!("Upload complete!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(moderation) = response.get("moderationResult") {
|
||||||
|
if let Some(state) = moderation.get("moderationState").and_then(|v| v.as_str()) {
|
||||||
|
log::info!("Moderation state: {}", state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::info!("Upload complete!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload via the Open Cloud Places API.
|
||||||
|
/// See https://create.roblox.com/docs/cloud/guides/usage-place-publishing
|
||||||
|
fn do_upload_place(
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
universe_id: u64,
|
universe_id: u64,
|
||||||
asset_id: u64,
|
asset_id: u64,
|
||||||
|
|
@ -149,20 +331,26 @@ fn do_upload_open_cloud(
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
log::debug!("Uploading to Roblox...");
|
log::debug!("Uploading to Roblox (Open Cloud Places API)...");
|
||||||
let response = client
|
let response = client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("x-api-key", api_key)
|
.header("x-api-key", api_key)
|
||||||
.header(CONTENT_TYPE, "application/xml")
|
.header(CONTENT_TYPE, "application/octet-stream")
|
||||||
.header(ACCEPT, "application/json")
|
.header(ACCEPT, "application/json")
|
||||||
.body(buffer)
|
.body(buffer)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
|
let body = response.text()?;
|
||||||
bail!(
|
bail!(
|
||||||
"The Roblox API returned an unexpected error: {}",
|
"The Roblox API returned HTTP {}: {}",
|
||||||
response.text()?
|
status,
|
||||||
|
if body.is_empty() {
|
||||||
|
"(empty response)".to_owned()
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ pub mod cli;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tree_view;
|
mod tree_view;
|
||||||
|
|
||||||
mod auth_cookie;
|
|
||||||
mod change_processor;
|
mod change_processor;
|
||||||
mod glob;
|
mod glob;
|
||||||
mod json;
|
mod json;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue