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
use std::{
    path::PathBuf,
    io,
};

use log::info;
use failure::Fail;

use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};

use crate::{
    imfs::{Imfs, FsError},
    project::{Project, ProjectLoadError},
    rbx_session::construct_oneoff_tree,
    rbx_snapshot::SnapshotError,
};

#[derive(Debug, Fail)]
pub enum UploadError {
    #[fail(display = "Roblox API Error: {}", _0)]
    RobloxApiError(String),

    #[fail(display = "Invalid asset kind: {}", _0)]
    InvalidKind(String),

    #[fail(display = "Project load error: {}", _0)]
    ProjectLoadError(#[fail(cause)] ProjectLoadError),

    #[fail(display = "IO error: {}", _0)]
    IoError(#[fail(cause)] io::Error),

    #[fail(display = "HTTP error: {}", _0)]
    HttpError(#[fail(cause)] reqwest::Error),

    #[fail(display = "XML model file error")]
    XmlModelEncodeError(rbx_xml::EncodeError),

    #[fail(display = "{}", _0)]
    FsError(#[fail(cause)] FsError),

    #[fail(display = "{}", _0)]
    SnapshotError(#[fail(cause)] SnapshotError),
}

impl_from!(UploadError {
    ProjectLoadError => ProjectLoadError,
    io::Error => IoError,
    reqwest::Error => HttpError,
    rbx_xml::EncodeError => XmlModelEncodeError,
    FsError => FsError,
    SnapshotError => SnapshotError,
});

#[derive(Debug)]
pub struct UploadOptions<'a> {
    pub fuzzy_project_path: PathBuf,
    pub security_cookie: String,
    pub asset_id: u64,
    pub kind: Option<&'a str>,
}

pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
    // TODO: Switch to uploading binary format?

    info!("Looking for project at {}", options.fuzzy_project_path.display());

    let project = Project::load_fuzzy(&options.fuzzy_project_path)?;

    info!("Found project at {}", project.file_location.display());
    info!("Using project {:#?}", project);

    let mut imfs = Imfs::new();
    imfs.add_roots_from_project(&project)?;
    let tree = construct_oneoff_tree(&project, &imfs)?;

    let root_id = tree.get_root_id();
    let mut contents = Vec::new();

    match options.kind {
        Some("place") | None => {
            let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
            rbx_xml::to_writer_default(&mut contents, &tree, top_level_ids)?;
        },
        Some("model") => {
            rbx_xml::to_writer_default(&mut contents, &tree, &[root_id])?;
        },
        Some(invalid) => return Err(UploadError::InvalidKind(invalid.to_owned())),
    }

    let url = format!("https://data.roblox.com/Data/Upload.ashx?assetid={}", options.asset_id);

    let client = reqwest::Client::new();
    let mut response = client.post(&url)
        .header(COOKIE, format!(".ROBLOSECURITY={}", &options.security_cookie))
        .header(USER_AGENT, "Roblox/WinInet")
        .header("Requester", "Client")
        .header(CONTENT_TYPE, "application/xml")
        .header(ACCEPT, "application/json")
        .body(contents)
        .send()?;

    if !response.status().is_success() {
        return Err(UploadError::RobloxApiError(response.text()?));
    }

    Ok(())
}