Last active
August 24, 2020 17:21
-
-
Save qryxip/e53b5d3d817ed7af81fda2c96d36fe36 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! ```cargo | |
//! [package] | |
//! name = "invesigate-dropbox" | |
//! version = "0.0.0" | |
//! authors = ["Ryo Yamashita <[email protected]>"] | |
//! edition = "2018" | |
//! publish = false | |
//! | |
//! [dependencies] | |
//! anyhow = "1.0.32" | |
//! atty = "0.2.14" | |
//! maplit = "1.0.2" | |
//! reqwest = { version = "0.10.7", features = ["blocking", "json"] } | |
//! serde = { version = "1.0.115", features = ["derive"] } | |
//! serde_json = "1.0.57" | |
//! structopt = "0.3.16" | |
//! termcolor = "1.1.0" | |
//! ``` | |
use anyhow::{anyhow, bail, Context as _}; | |
use maplit::btreeset; | |
use reqwest::Method; | |
use serde::Deserialize; | |
use serde_json::json; | |
use std::{ | |
collections::BTreeSet, | |
convert, env, fmt, | |
io::{self, Write as _}, | |
time::Duration, | |
}; | |
use structopt::StructOpt; | |
use termcolor::{BufferedStandardStream, Color, ColorChoice, ColorSpec, WriteColor as _}; | |
#[derive(StructOpt)] | |
struct Opt {} | |
fn main() -> anyhow::Result<()> { | |
Opt::from_args(); | |
let mut shell = Shell::new(); | |
let client = client()?; | |
let access_token = access_token()?; | |
let dir_names = list_folder(&mut shell, &client, &access_token, "")?; | |
let mut different = btreeset!(); | |
let mut arc_abcd = btreeset!(); | |
let mut arc_cdef = btreeset!(); | |
let mut arc_abcdef = btreeset!(); | |
for dir_name in dir_names { | |
let contest_ids = match &*dir_name { | |
"ARC060" => vec!["arc060".to_owned(), "abc044".to_owned()], | |
"ARC061" => vec!["arc061".to_owned(), "abc045".to_owned()], | |
"ARC062" => vec!["arc062".to_owned(), "abc046".to_owned()], | |
"ARC063" => vec!["arc063".to_owned(), "abc047".to_owned()], | |
"ARC064" => vec!["arc064".to_owned(), "abc048".to_owned()], | |
"ARC065" => vec!["arc065".to_owned(), "abc049".to_owned()], | |
"ARC066" => vec!["arc066".to_owned(), "abc050".to_owned()], | |
"ARC067" => vec!["arc067".to_owned(), "abc052".to_owned()], | |
"ARC068" => vec!["arc068".to_owned(), "abc053".to_owned()], | |
"ARC069" => vec!["arc069".to_owned(), "abc055".to_owned()], | |
"ARC070" => vec!["arc070".to_owned(), "abc056".to_owned()], | |
"ARC071" => vec!["arc071".to_owned(), "abc058".to_owned()], | |
"ARC072" => vec!["arc072".to_owned(), "abc059".to_owned()], | |
"ARC073" => vec!["arc073".to_owned(), "abc060".to_owned()], | |
"ARC074" => vec!["arc074".to_owned(), "abc062".to_owned()], | |
"ARC075" => vec!["arc075".to_owned(), "abc063".to_owned()], | |
"ARC076" => vec!["arc076".to_owned(), "abc065".to_owned()], | |
"ARC077" => vec!["arc077".to_owned(), "abc066".to_owned()], | |
"ARC078" => vec!["arc078".to_owned(), "abc067".to_owned()], | |
"ARC079" => vec!["arc079".to_owned(), "abc068".to_owned()], | |
"ARC080" => vec!["arc080".to_owned(), "abc069".to_owned()], | |
"ARC081" => vec!["arc081".to_owned(), "abc071".to_owned()], | |
"ARC082" => vec!["arc082".to_owned(), "abc072".to_owned()], | |
"ARC083" => vec!["arc083".to_owned(), "abc074".to_owned()], | |
"ARC084" => vec!["arc084".to_owned(), "abc077".to_owned()], | |
"ARC085" => vec!["arc085".to_owned(), "abc078".to_owned()], | |
"ARC086" => vec!["arc086".to_owned(), "abc081".to_owned()], | |
"ARC087" => vec!["arc087".to_owned(), "abc082".to_owned()], | |
"ARC088" => vec!["arc088".to_owned(), "abc083".to_owned()], | |
"ARC089" => vec!["arc089".to_owned(), "abc086".to_owned()], | |
"ARC090" => vec!["arc090".to_owned(), "abc087".to_owned()], | |
"ARC091" => vec!["arc091".to_owned(), "abc090".to_owned()], | |
"ARC092" => vec!["arc092".to_owned(), "abc091".to_owned()], | |
"ARC093" => vec!["arc093".to_owned(), "abc092".to_owned()], | |
"ARC094" => vec!["arc094".to_owned(), "abc093".to_owned()], | |
"ARC095" => vec!["arc095".to_owned(), "abc094".to_owned()], | |
"ARC096" => vec!["arc096".to_owned(), "abc095".to_owned()], | |
"ARC097" => vec!["arc097".to_owned(), "abc097".to_owned()], | |
"ARC098" => vec!["arc098".to_owned(), "abc098".to_owned()], | |
"ARC099" => vec!["arc099".to_owned(), "abc101".to_owned()], | |
"ARC100" => vec!["arc100".to_owned(), "abc102".to_owned()], | |
"ARC101" => vec!["arc101".to_owned(), "abc107".to_owned()], | |
"ARC102" => vec!["arc102".to_owned(), "abc108".to_owned()], | |
"ARC103" => vec!["arc103".to_owned(), "abc111".to_owned()], | |
"ARC058_ABC042" => vec!["arc058".to_owned(), "abc042".to_owned()], | |
"ARC059_ABC043" => vec!["arc059".to_owned(), "abc043".to_owned()], | |
"2019ddccqual" => vec!["ddcc2019-qual".to_owned()], | |
"2019exa" => vec!["exawizards2019".to_owned()], | |
"2019nikkei_qual" => vec!["nikkei2019-qual".to_owned()], | |
"2019yahoo_qual" => vec!["yahoo-procon2019-qual".to_owned()], | |
"2020_dwango_qual" => vec!["dwacon6th-prelims".to_owned()], | |
"2020_hitachi" => vec!["hitachi2020".to_owned()], | |
"2020_panasonic" => vec!["panasonic2020".to_owned()], | |
"CodeFestival2016EliminationTournament" => vec![ | |
"cf16-tournament-round1-open".to_owned(), | |
"cf16-tournament-round2-open".to_owned(), | |
"cf16-tournament-round3-open".to_owned(), | |
], | |
"CodeFestival2016Exhibition" => vec!["cf16-exhibition-open".to_owned()], | |
"CodeFestival2016Final" => vec!["cf16-final-open".to_owned()], | |
"CodeFestival2016GrandFinal" => vec!["cf16-exhibition-final-open".to_owned()], | |
"CodeFestival2016QualA" => vec!["code-festival-2016-quala".to_owned()], | |
"CodeFestival2016QualB" => vec!["code-festival-2016-qualb".to_owned()], | |
"CodeFestival2016QualC" => vec!["code-festival-2016-qualc".to_owned()], | |
"CodeFestival2016Relay" => vec!["cf16-relay-open".to_owned()], | |
"CodeFestival2017Exhibition" => vec!["cf17-exhibition-open".to_owned()], | |
"CodeFestival2017Final" => vec!["cf17-final-open".to_owned()], | |
"CodeFestival2017QualA" => vec!["code-festival-2017-quala".to_owned()], | |
"CodeFestival2017QualB" => vec!["code-festival-2017-qualb".to_owned()], | |
"CodeFestival2017QualC" => vec!["code-festival-2017-qualc".to_owned()], | |
"CodeFestival2017Relay" => vec!["cf17-relay-open".to_owned()], | |
"CodeFestival2017Tournament" => vec![ | |
"cf17-tournament-round1-open".to_owned(), | |
"cf17-tournament-round2-open".to_owned(), | |
"cf17-tournament-round3-open".to_owned(), | |
], | |
"MUJIN2017" => vec!["mujin-pc-2017".to_owned()], | |
"diverta2_2019" => vec!["diverta2019-2".to_owned()], | |
"diverta_2019" => vec!["diverta2019".to_owned()], | |
"jsc2019" => vec!["jsc2019-qual".to_owned()], | |
"msolutions2019" => vec!["m-solutions2019".to_owned()], | |
"nikkeiqual_2019" => vec!["nikkei2019-qual".to_owned()], | |
"tenka1_2017" => vec!["tenka1-2017".to_owned()], | |
"tenka1_2018" => vec!["tenka1-2018".to_owned()], | |
"tenka1_2019" => vec!["tenka1-2019".to_owned()], | |
dir_name => vec![dir_name.to_ascii_lowercase()], | |
}; | |
for contest_id in contest_ids { | |
if !exists(&mut shell, &client, &contest_id)? { | |
different.insert((dir_name.clone(), contest_id.clone())); | |
} | |
} | |
if dir_name.starts_with("ARC") && dir_name.len() == 6 { | |
let problems = list_folder( | |
&mut shell, | |
&client, | |
&access_token, | |
&format!("/{}", dir_name), | |
)?; | |
if problems.iter().eq(&["A", "B", "C", "D"]) { | |
arc_abcd.insert(dir_name); | |
} else if problems.iter().eq(&["C", "D", "E", "F"]) { | |
arc_cdef.insert(dir_name); | |
} else if problems.iter().eq(&["A", "B", "C", "D", "E", "F"]) { | |
arc_abcdef.insert(dir_name); | |
} else { | |
bail!("?????: {:?}", problems); | |
} | |
} | |
} | |
if different.is_empty() { | |
shell.info("found all")?; | |
} | |
for (dir_name, contest_id) in different { | |
shell.info(format!("not found: {:?} -> {:?}", dir_name, contest_id))?; | |
} | |
if !arc_abcd.is_empty() { | |
for name in arc_abcd { | |
shell.info(format!("ARC - A, B, C, D: {:?}", name))?; | |
} | |
} | |
if !arc_cdef.is_empty() { | |
for name in arc_cdef { | |
shell.info(format!("ARC - C, D, E, F: {:?}", name))?; | |
} | |
} | |
if !arc_abcdef.is_empty() { | |
for name in arc_abcdef { | |
shell.info(format!("ARC - A, B, C, D, E, F: {:?}", name))?; | |
} | |
} | |
Ok(()) | |
} | |
fn client() -> reqwest::Result<reqwest::blocking::Client> { | |
const USER_AGENT: &str = "[email protected]"; | |
const TIMEOUT: Option<Duration> = Some(Duration::from_secs(30)); | |
reqwest::blocking::ClientBuilder::new() | |
.user_agent(USER_AGENT) | |
.timeout(TIMEOUT) | |
.build() | |
} | |
fn access_token() -> anyhow::Result<String> { | |
env::var("DROPBOX_ACCESS_TOKEN").with_context(|| "could not read `$DROPBOX_ACCESS_TOKEN`") | |
} | |
fn list_folder( | |
shell: &mut Shell, | |
client: &reqwest::blocking::Client, | |
access_token: &str, | |
path: &str, | |
) -> anyhow::Result<BTreeSet<String>> { | |
static SHARED_LINK_URL: &str = | |
"https://www.dropbox.com/sh/arnpe0ef5wds8cv/AAAk_SECQ2Nc6SVGii3rHX6Fa?dl=0"; | |
#[derive(Deserialize)] | |
#[serde(untagged)] | |
enum ListFolderOutcome { | |
Ok(ListFolder), | |
Err(serde_json::Value), | |
} | |
#[derive(Deserialize)] | |
struct ListFolder { | |
entries: Vec<ListFolderEntry>, | |
} | |
#[derive(Deserialize)] | |
struct ListFolderEntry { | |
name: String, | |
} | |
let res = request_with_message( | |
shell, | |
client, | |
Method::POST, | |
"https://api.dropboxapi.com/2/files/list_folder", | |
|req| { | |
req.bearer_auth(access_token) | |
.json(&json!({ "shared_link": { "url": SHARED_LINK_URL }, "path": path })) | |
}, | |
)?; | |
if res.status() == 200 { | |
let ListFolder { entries } = res.json()?; | |
Ok(entries | |
.into_iter() | |
.map(|ListFolderEntry { name }| name) | |
.collect()) | |
} else { | |
let msg = res.text()?; | |
let msg = serde_json::to_string_pretty(&msg).unwrap_or(msg); | |
Err(anyhow!("{}", msg).context("API error")) | |
} | |
} | |
fn exists( | |
shell: &mut Shell, | |
client: &reqwest::blocking::Client, | |
contest_id: &str, | |
) -> anyhow::Result<bool> { | |
let status = request_with_message( | |
shell, | |
client, | |
Method::GET, | |
&format!("https://atcoder.jp/contests/{}", contest_id), | |
convert::identity, | |
)? | |
.status(); | |
match status.as_u16() { | |
200 => Ok(true), | |
404 => Ok(false), | |
_ => bail!("{}", status), | |
} | |
} | |
fn request_with_message( | |
shell: &mut Shell, | |
client: &reqwest::blocking::Client, | |
method: Method, | |
url: &str, | |
modify_req: impl FnOnce(reqwest::blocking::RequestBuilder) -> reqwest::blocking::RequestBuilder, | |
) -> anyhow::Result<reqwest::blocking::Response> { | |
let req = modify_req(client.request(method.clone(), url)); | |
shell.network(format!("{}: {}", method, url))?; | |
let res = req.send()?; | |
shell.network(res.status())?; | |
Ok(res) | |
} | |
struct Shell(BufferedStandardStream); | |
impl Shell { | |
fn new() -> Self { | |
Self(BufferedStandardStream::stderr( | |
if atty::is(atty::Stream::Stderr) { | |
ColorChoice::Auto | |
} else { | |
ColorChoice::Never | |
}, | |
)) | |
} | |
fn info(&mut self, message: impl fmt::Display) -> io::Result<()> { | |
self.print("info:", message, Color::Cyan) | |
} | |
fn network(&mut self, message: impl fmt::Display) -> io::Result<()> { | |
self.print("network:", message, Color::Magenta) | |
} | |
fn print( | |
&mut self, | |
status: impl fmt::Display, | |
message: impl fmt::Display, | |
color: Color, | |
) -> io::Result<()> { | |
self.0 | |
.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; | |
write!(self.0, "{}", status)?; | |
self.0.reset()?; | |
writeln!(self.0, " {}", message)?; | |
self.0.flush() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://gist.github.com/qryxip/ff63fef4461c1ab08e64130696ca6e7c