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

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

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

use crate::{
    rbx_session::construct_oneoff_tree,
    project::{Project, ProjectLoadFuzzyError},
    imfs::Imfs,
};

#[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)] ProjectLoadFuzzyError),

    #[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),
}

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

#[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::encode(&tree, top_level_ids, &mut contents)?;
        },
        Some("model") => {
            rbx_xml::encode(&tree, &[root_id], &mut contents)?;
        },
        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(())
}