Skip to content

Commit 1215e6f

Browse files
committed
GitHub backend, refactor a bit
1 parent ddfa731 commit 1215e6f

7 files changed

Lines changed: 229 additions & 128 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ name = "drillbit"
33
version = "0.1.4"
44
edition = "2024"
55
description = " A plugin installation tool for Roblox"
6-
license = "MIT"
7-
authors = ["Jack Taylor <j@jackt.io>"]
8-
repository = "https://github.com/jacktabscode/drillbit"
9-
homepage = "https://github.com/jacktabscode/drillbit"
106
readme = "README.md"
7+
homepage = "https://github.com/jacktabscode/drillbit"
8+
repository = "https://github.com/jacktabscode/drillbit"
9+
license = "MIT"
1110

1211
[dependencies]
1312
anyhow = "1.0.99"
13+
async-trait = "0.1.86"
1414
blake3 = "1.8.2"
1515
env_logger = "0.11.8"
1616
fs-err = { version = "3.1.1", features = ["tokio"] }

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ local = "plugins/editor_position.luau" # note that Roblox doesn't load `.luau` f
3030
# You can also add plugins on the Creator Store
3131
[plugins.hoarcekat]
3232
cloud = 4621580428
33+
34+
# You can also use GitHub release artifacts
35+
[plugins.jest_companion]
36+
github = "https://github.com/jackTabsCode/jest-companion/releases/download/v0.1.1/plugin.rbxm"
3337
```
3438

3539
## Usage

src/backends.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use anyhow::{Context, Result, bail};
2+
use async_trait::async_trait;
3+
use fs_err::tokio as fs;
4+
use reqwest::Client;
5+
use serde::Deserialize;
6+
7+
use crate::config::Plugin;
8+
9+
#[async_trait]
10+
pub trait Backend {
11+
fn new() -> Result<Self>
12+
where
13+
Self: Sized;
14+
15+
async fn download(&mut self, plugin: &Plugin) -> Result<(Vec<u8>, Option<String>)>;
16+
17+
fn plugin_id(&self, plugin: &Plugin, key: &str, cwd: &str) -> String;
18+
}
19+
20+
pub struct LocalBackend;
21+
22+
#[async_trait]
23+
impl Backend for LocalBackend {
24+
fn new() -> Result<Self> {
25+
Ok(LocalBackend)
26+
}
27+
28+
async fn download(&mut self, plugin: &Plugin) -> Result<(Vec<u8>, Option<String>)> {
29+
let Plugin::Local(path) = plugin else {
30+
bail!("LocalBackend can only handle Local plugins")
31+
};
32+
33+
let mut new_ext = None;
34+
35+
if let Some(ext) = path.extension()
36+
&& ext == "luau"
37+
{
38+
new_ext = Some("lua".to_string());
39+
}
40+
41+
let path = path.to_path(".");
42+
let data = fs::read(path).await.context("Failed to read plugin")?;
43+
44+
Ok((data, new_ext))
45+
}
46+
47+
fn plugin_id(&self, plugin: &Plugin, _key: &str, cwd: &str) -> String {
48+
let Plugin::Local(path) = plugin else {
49+
unreachable!()
50+
};
51+
52+
let filename = path
53+
.to_path(".")
54+
.file_name()
55+
.unwrap()
56+
.to_string_lossy()
57+
.to_string();
58+
59+
format!("{}_{}", cwd, filename)
60+
}
61+
}
62+
63+
pub struct CloudBackend {
64+
cookie: Option<String>,
65+
client: Client,
66+
}
67+
68+
impl CloudBackend {
69+
fn get_cookie(&mut self) -> Result<String> {
70+
if self.cookie.is_none() {
71+
let cookie = rbx_cookie::get().context("Couldn't get Roblox cookie")?;
72+
self.cookie = Some(cookie);
73+
}
74+
Ok(self.cookie.as_ref().unwrap().clone())
75+
}
76+
}
77+
78+
#[async_trait]
79+
impl Backend for CloudBackend {
80+
fn new() -> Result<Self> {
81+
Ok(Self {
82+
cookie: None,
83+
client: Client::new(),
84+
})
85+
}
86+
87+
async fn download(&mut self, plugin: &Plugin) -> Result<(Vec<u8>, Option<String>)> {
88+
let Plugin::Cloud(id) = plugin else {
89+
unreachable!()
90+
};
91+
92+
let cookie = self.get_cookie()?;
93+
let url = format!("https://assetdelivery.roblox.com/v2/asset?id={id}");
94+
let res = self
95+
.client
96+
.get(&url)
97+
.header("Cookie", &cookie)
98+
.send()
99+
.await?;
100+
101+
if !res.status().is_success() {
102+
bail!("Request failed with status: {}", res.status());
103+
}
104+
105+
let asset: AssetResponse = res.json().await?;
106+
let download_url = asset
107+
.locations
108+
.first()
109+
.ok_or_else(|| anyhow::anyhow!("No download locations found"))?
110+
.location
111+
.clone();
112+
113+
let file_res = self
114+
.client
115+
.get(download_url)
116+
.header("Cookie", &cookie)
117+
.send()
118+
.await?;
119+
120+
if !file_res.status().is_success() {
121+
bail!("Download failed with status: {}", file_res.status());
122+
}
123+
124+
Ok((file_res.bytes().await?.to_vec(), Some("rbxm".to_string())))
125+
}
126+
127+
fn plugin_id(&self, plugin: &Plugin, key: &str, cwd: &str) -> String {
128+
let Plugin::Cloud(id) = plugin else {
129+
unreachable!()
130+
};
131+
132+
format!("{}_{}_{}", cwd, key, id)
133+
}
134+
}
135+
136+
pub struct GitHubBackend {
137+
client: Client,
138+
}
139+
140+
#[async_trait]
141+
impl Backend for GitHubBackend {
142+
fn new() -> Result<Self> {
143+
Ok(Self {
144+
client: Client::new(),
145+
})
146+
}
147+
148+
async fn download(&mut self, plugin: &Plugin) -> Result<(Vec<u8>, Option<String>)> {
149+
let Plugin::GitHub(url) = plugin else {
150+
unimplemented!()
151+
};
152+
153+
let res = self.client.get(url).send().await?;
154+
155+
if !res.status().is_success() {
156+
bail!(
157+
"GitHub release download failed with status: {}",
158+
res.status()
159+
);
160+
}
161+
162+
let ext = url.split('.').next_back().map(|s| s.to_string());
163+
Ok((res.bytes().await?.to_vec(), ext))
164+
}
165+
166+
fn plugin_id(&self, plugin: &Plugin, _key: &str, cwd: &str) -> String {
167+
let Plugin::GitHub(url) = plugin else {
168+
unimplemented!()
169+
};
170+
171+
let filename = url.split('/').next_back().unwrap_or("unknown");
172+
format!("{}_{}", cwd, filename)
173+
}
174+
}
175+
176+
#[derive(Deserialize)]
177+
struct Location {
178+
location: String,
179+
}
180+
181+
#[derive(Deserialize)]
182+
struct AssetResponse {
183+
locations: Vec<Location>,
184+
}

src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ impl Config {
4848
}
4949

5050
#[derive(Debug, Deserialize)]
51-
#[serde(rename_all = "snake_case")]
51+
#[serde(rename_all = "lowercase")]
5252
pub enum Plugin {
5353
Local(RelativePathBuf),
5454
Cloud(u64),
55+
GitHub(String),
5556
}

src/main.rs

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
2+
backends::{Backend, CloudBackend, GitHubBackend, LocalBackend},
23
config::{Config, Plugin},
3-
web::WebClient,
44
};
55
use anyhow::{Context, bail};
66
use fs_err::tokio as fs;
@@ -12,8 +12,8 @@ use std::{
1212
path::{Path, PathBuf},
1313
};
1414

15+
mod backends;
1516
mod config;
16-
mod web;
1717

1818
#[tokio::main]
1919
async fn main() -> anyhow::Result<()> {
@@ -24,29 +24,36 @@ async fn main() -> anyhow::Result<()> {
2424

2525
let config = Config::read().await.context("Failed to read config")?;
2626

27-
let has_cloud_plugins = config
28-
.plugins
29-
.values()
30-
.any(|plugin| matches!(plugin, Plugin::Cloud(_)));
31-
let client = if has_cloud_plugins {
32-
Some(WebClient::new()?)
33-
} else {
34-
None
35-
};
36-
3727
let cwd_path = env::current_dir().unwrap();
3828
let cwd = cwd_path.file_name().unwrap().to_str().unwrap();
3929

4030
let plugins_path = get_plugins_path().context("Failed to get plugins path")?;
4131

4232
let mut existing_plugins = get_existing_hashes(&plugins_path).await?;
4333

34+
fn get_or_create_backend<T: Backend>(backend: &mut Option<T>) -> anyhow::Result<&mut T> {
35+
if backend.is_none() {
36+
*backend = Some(T::new()?);
37+
}
38+
Ok(backend.as_mut().unwrap())
39+
}
40+
41+
let mut local_backend: Option<LocalBackend> = None;
42+
let mut github_backend: Option<GitHubBackend> = None;
43+
let mut cloud_backend: Option<CloudBackend> = None;
44+
4445
for (key, plugin) in config.plugins {
45-
let id = plugin_id(&plugin, &key, cwd);
46-
let mut path = plugins_path.join(&id);
46+
let backend: &mut dyn Backend = match &plugin {
47+
Plugin::Local(_) => get_or_create_backend(&mut local_backend)?,
48+
Plugin::GitHub(_) => get_or_create_backend(&mut github_backend)?,
49+
Plugin::Cloud(_) => get_or_create_backend(&mut cloud_backend)?,
50+
};
4751

52+
let id = backend.plugin_id(&plugin, &key, cwd);
4853
info!("Reading \"{key}\"...");
49-
let (data, ext) = read_plugin(client.as_ref(), &plugin).await?;
54+
let (data, ext) = backend.download(&plugin).await?;
55+
56+
let mut path = plugins_path.join(&id);
5057

5158
if let Some(ext) = ext {
5259
path.set_extension(ext);
@@ -110,50 +117,3 @@ async fn get_existing_hashes(
110117

111118
Ok(existing_plugins)
112119
}
113-
114-
fn plugin_id(plugin: &Plugin, key: &str, cwd: &str) -> String {
115-
match plugin {
116-
Plugin::Local(path) => {
117-
let filename = path
118-
.to_path(".")
119-
.file_name()
120-
.unwrap()
121-
.to_string_lossy()
122-
.to_string();
123-
format!("{}_{}", cwd, filename)
124-
}
125-
Plugin::Cloud(id) => {
126-
format!("{}_{}_{}", cwd, key, id)
127-
}
128-
}
129-
}
130-
131-
async fn read_plugin(
132-
client: Option<&WebClient>,
133-
plugin: &Plugin,
134-
) -> anyhow::Result<(Vec<u8>, Option<String>)> {
135-
match plugin {
136-
Plugin::Cloud(id) => {
137-
let client = client.context("WebClient is required for cloud plugins")?;
138-
let data = client
139-
.download_plugin(*id)
140-
.await
141-
.context("Failed to download plugin")?;
142-
143-
Ok((data, Some("rbxm".to_string())))
144-
}
145-
Plugin::Local(path) => {
146-
let mut new_ext = None;
147-
148-
if let Some(ext) = path.extension()
149-
&& ext == "luau"
150-
{
151-
new_ext = Some("lua".to_string());
152-
}
153-
let path = path.to_path(".");
154-
let data = fs::read(path).await.context("Failed to read plugin")?;
155-
156-
Ok((data, new_ext))
157-
}
158-
}
159-
}

0 commit comments

Comments
 (0)