use crate::error::{Error, Result};
use crate::sprite::Sprite;
use chrono::{DateTime, Utc};
use serde::Deserialize;
#[derive(Clone)]
pub struct Filesystem {
sprite: Sprite,
cwd: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FileInfo {
pub name: String,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub mode: u32,
#[serde(default, alias = "mod_time")]
pub modified: Option<DateTime<Utc>>,
#[serde(default)]
pub is_dir: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DirEntry {
pub name: String,
#[serde(default)]
pub is_dir: bool,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub mode: u32,
#[serde(default, alias = "mod_time")]
pub modified: Option<DateTime<Utc>>,
}
impl Filesystem {
pub(crate) fn new(sprite: Sprite) -> Self {
Self { sprite, cwd: None }
}
pub(crate) fn with_cwd(sprite: Sprite, cwd: String) -> Self {
Self {
sprite,
cwd: Some(cwd),
}
}
pub fn cd(&self, path: &str) -> Filesystem {
let new_cwd = if path.starts_with('/') {
path.to_string()
} else if let Some(ref cwd) = self.cwd {
format!("{}/{}", cwd.trim_end_matches('/'), path)
} else {
format!("/{path}")
};
Filesystem {
sprite: self.sprite.clone(),
cwd: Some(new_cwd),
}
}
fn resolve_path(&self, path: &str) -> String {
if path.starts_with('/') {
path.to_string()
} else if let Some(ref cwd) = self.cwd {
format!("{}/{}", cwd.trim_end_matches('/'), path)
} else {
format!("/{path}")
}
}
fn fs_url(&self, path: &str) -> String {
let resolved = self.resolve_path(path);
let encoded = resolved
.split('/')
.map(|segment| urlencoding::encode(segment).into_owned())
.collect::<Vec<_>>()
.join("/");
self.sprite
.client()
.url(&format!("/sprites/{}/fs{}", self.sprite.name(), encoded))
}
pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
let url = self.fs_url(path);
let response = self
.sprite
.client()
.http()
.get(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(Error::not_found(path));
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
pub async fn read_to_string(&self, path: &str) -> Result<String> {
let data = self.read_file(path).await?;
Ok(String::from_utf8_lossy(&data).to_string())
}
pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<()> {
let url = self.fs_url(path);
let response = self
.sprite
.client()
.http()
.put(&url)
.header("Authorization", self.sprite.client().auth_header())
.header("Content-Type", "application/octet-stream")
.body(data.to_vec())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn write_file_with_mode(&self, path: &str, data: &[u8], mode: u32) -> Result<()> {
self.write_file(path, data).await?;
self.chmod(path, mode).await
}
pub async fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
let url = format!("{}?list=true", self.fs_url(path));
let response = self
.sprite
.client()
.http()
.get(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(Error::not_found(path));
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
let entries: Vec<DirEntry> = response.json().await?;
Ok(entries)
}
pub async fn mkdir(&self, path: &str) -> Result<()> {
let url = format!("{}?mkdir=true", self.fs_url(path));
let response = self
.sprite
.client()
.http()
.post(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn mkdir_all(&self, path: &str) -> Result<()> {
let url = format!("{}?mkdir=true&parents=true", self.fs_url(path));
let response = self
.sprite
.client()
.http()
.post(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn remove(&self, path: &str) -> Result<()> {
let url = self.fs_url(path);
let response = self
.sprite
.client()
.http()
.delete(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(Error::not_found(path));
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn remove_all(&self, path: &str) -> Result<()> {
let url = format!("{}?recursive=true", self.fs_url(path));
let response = self
.sprite
.client()
.http()
.delete(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(Error::not_found(path));
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn rename(&self, from: &str, to: &str) -> Result<()> {
let from_path = self.resolve_path(from);
let to_path = self.resolve_path(to);
let url = self.sprite.client().url(&format!(
"/sprites/{}/fs?rename={}&to={}",
self.sprite.name(),
urlencoding::encode(&from_path),
urlencoding::encode(&to_path)
));
let response = self
.sprite
.client()
.http()
.post(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn copy(&self, from: &str, to: &str) -> Result<()> {
let from_path = self.resolve_path(from);
let to_path = self.resolve_path(to);
let url = self.sprite.client().url(&format!(
"/sprites/{}/fs?copy={}&to={}",
self.sprite.name(),
urlencoding::encode(&from_path),
urlencoding::encode(&to_path)
));
let response = self
.sprite
.client()
.http()
.post(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn chmod(&self, path: &str, mode: u32) -> Result<()> {
let url = format!("{}?chmod={:o}", self.fs_url(path), mode);
let response = self
.sprite
.client()
.http()
.post(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
Ok(())
}
pub async fn stat(&self, path: &str) -> Result<FileInfo> {
let url = format!("{}?stat=true", self.fs_url(path));
let response = self
.sprite
.client()
.http()
.get(&url)
.header("Authorization", self.sprite.client().auth_header())
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(Error::not_found(path));
}
if !status.is_success() {
let message = response.text().await.unwrap_or_default();
return Err(Error::api(status.as_u16(), message));
}
let info: FileInfo = response.json().await?;
Ok(info)
}
pub async fn exists(&self, path: &str) -> Result<bool> {
match self.stat(path).await {
Ok(_) => Ok(true),
Err(Error::NotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
}
mod urlencoding {
pub fn encode(s: &str) -> std::borrow::Cow<'_, str> {
let mut needs_encoding = false;
for c in s.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => {}
_ => {
needs_encoding = true;
break;
}
}
}
if !needs_encoding {
return std::borrow::Cow::Borrowed(s);
}
let mut result = String::new();
for c in s.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
_ => {
for byte in c.to_string().as_bytes() {
result.push_str(&format!("%{byte:02X}"));
}
}
}
}
std::borrow::Cow::Owned(result)
}
}