Merge branch 'main' into app-users-orgs-and-more
This commit is contained in:
114 files changed
+2832
-1618
No files matched your search
@@ -0,0 +1,2 @@
|
||||
[env]
|
||||
SQLX_OFFLINE = "true"
|
||||
@@ -29,6 +29,7 @@ regex = "1.5"
|
||||
sys-info = "0.9.0"
|
||||
sysinfo = "0.30.8"
|
||||
thiserror = "1.0"
|
||||
either = "1.13"
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
|
||||
@@ -62,6 +63,8 @@ base64 = "0.22.0"
|
||||
|
||||
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "sqlite", "macros" ] }
|
||||
|
||||
ariadne = { path = "../ariadne" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52.0"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::state::{FriendsSocket, UserFriend, UserStatus};
|
||||
use crate::state::{FriendsSocket, UserFriend};
|
||||
use ariadne::users::UserStatus;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn friends() -> crate::Result<Vec<UserFriend>> {
|
||||
|
||||
@@ -19,8 +19,9 @@ pub mod data {
|
||||
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
|
||||
ModrinthCredentials, Organization, ProcessMetadata, ProfileFile,
|
||||
Project, ProjectType, SearchResult, SearchResults, Settings,
|
||||
TeamMember, Theme, User, UserFriend, UserStatus, Version, WindowSize,
|
||||
TeamMember, Theme, User, UserFriend, Version, WindowSize,
|
||||
};
|
||||
pub use ariadne::users::UserStatus;
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
|
||||
@@ -13,6 +13,11 @@ pub enum ErrorKind {
|
||||
#[error("Serialization error (JSON): {0}")]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Serialization error (websocket): {0}")]
|
||||
WebsocketSerializationError(
|
||||
#[from] ariadne::networking::serialization::SerializationError,
|
||||
),
|
||||
|
||||
#[error("Error parsing UUID: {0}")]
|
||||
UUIDError(#[from] uuid::Error),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Theseus state management system
|
||||
use crate::state::UserStatus;
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
@@ -262,8 +262,8 @@ pub enum EventError {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "event")]
|
||||
pub enum FriendPayload {
|
||||
FriendRequest { from: String },
|
||||
UserOffline { id: String },
|
||||
FriendRequest { from: UserId },
|
||||
UserOffline { id: UserId },
|
||||
StatusUpdate { user_status: UserStatus },
|
||||
StatusSync,
|
||||
}
|
||||
@@ -209,34 +209,6 @@ impl DirectoryInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_same_disk(
|
||||
old_dir: &Path,
|
||||
new_dir: &Path,
|
||||
) -> crate::Result<bool> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
Ok(old_dir.metadata()?.dev() == new_dir.metadata()?.dev())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let old_dir = crate::util::io::canonicalize(old_dir)?;
|
||||
let new_dir = crate::util::io::canonicalize(new_dir)?;
|
||||
|
||||
let old_component = old_dir.components().next();
|
||||
let new_component = new_dir.components().next();
|
||||
|
||||
match (old_component, new_component) {
|
||||
(
|
||||
Some(std::path::Component::Prefix(old)),
|
||||
Some(std::path::Component::Prefix(new)),
|
||||
) => Ok(old.as_os_str() == new.as_os_str()),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_disk_usage(path: &Path) -> crate::Result<Option<u64>> {
|
||||
let path = crate::util::io::canonicalize(path)?;
|
||||
|
||||
@@ -335,7 +307,9 @@ impl DirectoryInfo {
|
||||
|
||||
let paths_len = paths.len();
|
||||
|
||||
if is_same_disk(&prev_dir, &move_dir).unwrap_or(false) {
|
||||
if crate::util::io::is_same_disk(&prev_dir, &move_dir)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let success_idxs = Arc::new(DashSet::new());
|
||||
|
||||
let loader_bar_id = Arc::new(&loader_bar_id);
|
||||
@@ -359,7 +333,7 @@ impl DirectoryInfo {
|
||||
})?;
|
||||
}
|
||||
|
||||
crate::util::io::rename(
|
||||
crate::util::io::rename_or_move(
|
||||
&x.old,
|
||||
&x.new,
|
||||
)
|
||||
|
||||
@@ -2,28 +2,42 @@ use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::emit::emit_friend;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::state::{ProcessManager, Profile};
|
||||
use crate::state::tunnel::InternalTunnelSocket;
|
||||
use crate::state::{ProcessManager, Profile, TunnelSocket};
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore};
|
||||
use ariadne::networking::message::{
|
||||
ClientToServerMessage, ServerToClientMessage,
|
||||
};
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use async_tungstenite::tokio::{connect_async, ConnectStream};
|
||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use async_tungstenite::tungstenite::Message;
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use either::Either;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::OwnedReadHalf;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
type WriteSocket =
|
||||
pub(super) type WriteSocket =
|
||||
Arc<RwLock<Option<SplitSink<WebSocketStream<ConnectStream>, Message>>>>;
|
||||
pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
|
||||
|
||||
pub struct FriendsSocket {
|
||||
write: WriteSocket,
|
||||
user_statuses: Arc<DashMap<String, UserStatus>>,
|
||||
user_statuses: Arc<DashMap<UserId, UserStatus>>,
|
||||
tunnel_sockets: TunnelSockets,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -34,28 +48,6 @@ pub struct UserFriend {
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientToServerMessage {
|
||||
StatusUpdate { profile_name: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerToClientMessage {
|
||||
StatusUpdate { status: UserStatus },
|
||||
UserOffline { id: String },
|
||||
FriendStatuses { statuses: Vec<UserStatus> },
|
||||
FriendRequest { from: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserStatus {
|
||||
pub user_id: String,
|
||||
pub profile_name: Option<String>,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for FriendsSocket {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
@@ -67,6 +59,7 @@ impl FriendsSocket {
|
||||
Self {
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
user_statuses: Arc::new(DashMap::new()),
|
||||
tunnel_sockets: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +113,7 @@ impl FriendsSocket {
|
||||
|
||||
let write_handle = self.write.clone();
|
||||
let statuses = self.user_statuses.clone();
|
||||
let sockets = self.tunnel_sockets.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut read_stream = read;
|
||||
@@ -128,18 +122,14 @@ impl FriendsSocket {
|
||||
Ok(msg) => {
|
||||
let server_message = match msg {
|
||||
Message::Text(text) => {
|
||||
serde_json::from_str::<
|
||||
ServerToClientMessage,
|
||||
>(
|
||||
&text
|
||||
ServerToClientMessage::deserialize(
|
||||
Either::Left(&text),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
Message::Binary(bytes) => {
|
||||
serde_json::from_slice::<
|
||||
ServerToClientMessage,
|
||||
>(
|
||||
&bytes
|
||||
ServerToClientMessage::deserialize(
|
||||
Either::Right(&bytes),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
@@ -165,7 +155,7 @@ impl FriendsSocket {
|
||||
{
|
||||
match server_message {
|
||||
ServerToClientMessage::StatusUpdate { status } => {
|
||||
statuses.insert(status.user_id.clone(), status.clone());
|
||||
statuses.insert(status.user_id, status.clone());
|
||||
let _ = emit_friend(FriendPayload::StatusUpdate { user_status: status }).await;
|
||||
},
|
||||
ServerToClientMessage::UserOffline { id } => {
|
||||
@@ -175,13 +165,41 @@ impl FriendsSocket {
|
||||
ServerToClientMessage::FriendStatuses { statuses: new_statuses } => {
|
||||
statuses.clear();
|
||||
new_statuses.into_iter().for_each(|status| {
|
||||
statuses.insert(status.user_id.clone(), status);
|
||||
statuses.insert(status.user_id, status);
|
||||
});
|
||||
let _ = emit_friend(FriendPayload::StatusSync).await;
|
||||
}
|
||||
ServerToClientMessage::FriendRequest { from } => {
|
||||
let _ = emit_friend(FriendPayload::FriendRequest { from }).await;
|
||||
}
|
||||
ServerToClientMessage::FriendRequestRejected { .. } => todo!(),
|
||||
|
||||
ServerToClientMessage::FriendSocketListening { .. } => {}, // TODO
|
||||
ServerToClientMessage::FriendSocketStoppedListening { .. } => {}, // TODO
|
||||
|
||||
ServerToClientMessage::SocketConnected { to_socket, new_socket } => {
|
||||
if let Some(connected_to) = sockets.get(&to_socket) {
|
||||
if let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone() {
|
||||
if let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
||||
let (read, write) = new_stream.into_split();
|
||||
sockets.insert(new_socket, Arc::new(InternalTunnelSocket::Connected(Mutex::new(write))));
|
||||
Self::socket_read_loop(write_handle.clone(), read, new_socket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = Self::send_message(&write_handle, ClientToServerMessage::SocketClose { socket: new_socket }).await;
|
||||
},
|
||||
ServerToClientMessage::SocketClosed { socket } => {
|
||||
sockets.remove_if(&socket, |_, x| matches!(*x.clone(), InternalTunnelSocket::Connected(_)));
|
||||
},
|
||||
ServerToClientMessage::SocketData { socket, data } => {
|
||||
if let Some(mut socket) = sockets.get_mut(&socket) {
|
||||
if let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
||||
let _ = stream.lock().await.write_all(&data).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,10 +235,7 @@ impl FriendsSocket {
|
||||
let mut last_ping = Utc::now();
|
||||
|
||||
loop {
|
||||
let connected = {
|
||||
let read = state.friends_socket.write.read().await;
|
||||
read.is_some()
|
||||
};
|
||||
let connected = state.friends_socket.is_connected().await;
|
||||
|
||||
if !connected
|
||||
&& Utc::now().signed_duration_since(last_connection)
|
||||
@@ -269,16 +284,11 @@ impl FriendsSocket {
|
||||
&self,
|
||||
profile_name: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
let mut write_lock = self.write.write().await;
|
||||
if let Some(ref mut write_half) = *write_lock {
|
||||
write_half
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientToServerMessage::StatusUpdate { profile_name },
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Self::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::StatusUpdate { profile_name },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
@@ -346,4 +356,81 @@ impl FriendsSocket {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn open_port(&self, port: u16) -> crate::Result<TunnelSocket> {
|
||||
let socket_id = Uuid::new_v4();
|
||||
let socket = self.tunnel_sockets.entry(socket_id).insert(Arc::new(
|
||||
InternalTunnelSocket::Listening(SocketAddr::new(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
port,
|
||||
)),
|
||||
));
|
||||
Self::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::SocketListen { socket: socket_id },
|
||||
)
|
||||
.await?;
|
||||
self.create_tunnel_socket(socket_id, socket)
|
||||
}
|
||||
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
self.write.read().await.is_some()
|
||||
}
|
||||
|
||||
fn create_tunnel_socket(
|
||||
&self,
|
||||
socket_id: Uuid,
|
||||
socket: impl Deref<Target = Arc<InternalTunnelSocket>>,
|
||||
) -> crate::Result<TunnelSocket> {
|
||||
Ok(TunnelSocket {
|
||||
socket_id,
|
||||
write: self.write.clone(),
|
||||
sockets: self.tunnel_sockets.clone(),
|
||||
internal: socket.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn socket_read_loop(
|
||||
write: WriteSocket,
|
||||
mut read_half: OwnedReadHalf,
|
||||
socket_id: Uuid,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut read_buffer = [0u8; 8192];
|
||||
loop {
|
||||
match read_half.read(&mut read_buffer).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
let _ = Self::send_message(
|
||||
&write,
|
||||
ClientToServerMessage::SocketSend {
|
||||
socket: socket_id,
|
||||
data: read_buffer[..n].to_vec(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(write))]
|
||||
pub(super) async fn send_message(
|
||||
write: &WriteSocket,
|
||||
message: ClientToServerMessage,
|
||||
) -> crate::Result<()> {
|
||||
let serialized = match message.serialize()? {
|
||||
Either::Left(text) => Message::text(text),
|
||||
Either::Right(bytes) => Message::binary(bytes),
|
||||
};
|
||||
|
||||
let mut write_lock = write.write().await;
|
||||
if let Some(ref mut write_half) = *write_lock {
|
||||
write_half.send(serialized).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ pub use self::cache::*;
|
||||
mod friends;
|
||||
pub use self::friends::*;
|
||||
|
||||
mod tunnel;
|
||||
pub use self::tunnel::*;
|
||||
|
||||
pub mod db;
|
||||
pub mod fs_watcher;
|
||||
mod mr_auth;
|
||||
|
||||
@@ -928,7 +928,8 @@ impl Profile {
|
||||
format!("{project_path}.disabled")
|
||||
};
|
||||
|
||||
io::rename(&path.join(project_path), &path.join(&new_path)).await?;
|
||||
io::rename_or_move(&path.join(project_path), &path.join(&new_path))
|
||||
.await?;
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use crate::state::friends::{TunnelSockets, WriteSocket};
|
||||
use crate::state::FriendsSocket;
|
||||
use ariadne::networking::message::ClientToServerMessage;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(super) enum InternalTunnelSocket {
|
||||
Listening(SocketAddr),
|
||||
Connected(Mutex<OwnedWriteHalf>),
|
||||
}
|
||||
|
||||
pub struct TunnelSocket {
|
||||
pub(super) socket_id: Uuid,
|
||||
pub(super) write: WriteSocket,
|
||||
pub(super) sockets: TunnelSockets,
|
||||
pub(super) internal: Arc<InternalTunnelSocket>,
|
||||
}
|
||||
|
||||
impl TunnelSocket {
|
||||
pub fn socket_id(&self) -> Uuid {
|
||||
self.socket_id
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> crate::Result<()> {
|
||||
if self.sockets.remove(&self.socket_id).is_some() {
|
||||
FriendsSocket::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::SocketClose {
|
||||
socket: self.socket_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if let InternalTunnelSocket::Connected(ref stream) =
|
||||
*self.internal.clone()
|
||||
{
|
||||
stream.lock().await.shutdown().await?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TunnelSocket {
|
||||
fn drop(&mut self) {
|
||||
if self.sockets.remove(&self.socket_id).is_some() {
|
||||
let write = self.write.clone();
|
||||
let socket_id = self.socket_id;
|
||||
tokio::spawn(async move {
|
||||
let _ = FriendsSocket::send_message(
|
||||
&write,
|
||||
ClientToServerMessage::SocketClose { socket: socket_id },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Functions for fetching infromation from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::event::emit::emit_loading;
|
||||
use crate::event::LoadingBarId;
|
||||
use bytes::Bytes;
|
||||
@@ -11,8 +13,6 @@ use std::time::{self};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use super::io::{self, IOError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IoSemaphore(pub Semaphore);
|
||||
#[derive(Debug)]
|
||||
@@ -87,7 +87,8 @@ pub async fn fetch_advanced(
|
||||
.map(|x| &*x.0.to_lowercase() == "authorization")
|
||||
.unwrap_or(false)
|
||||
&& (url.starts_with("https://cdn.modrinth.com")
|
||||
|| url.starts_with("https://api.modrinth.com"))
|
||||
|| url.starts_with(MODRINTH_API_URL)
|
||||
|| url.starts_with(MODRINTH_API_URL_V3))
|
||||
{
|
||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||
} else {
|
||||
|
||||
@@ -59,6 +59,19 @@ pub async fn read_dir(
|
||||
})
|
||||
}
|
||||
|
||||
// create_dir
|
||||
pub async fn create_dir(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::create_dir(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// create_dir_all
|
||||
pub async fn create_dir_all(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
@@ -150,19 +163,72 @@ fn sync_write(
|
||||
tmp_path.persist(path)?;
|
||||
std::io::Result::Ok(())
|
||||
}
|
||||
|
||||
pub fn is_same_disk(old_dir: &Path, new_dir: &Path) -> Result<bool, IOError> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
Ok(old_dir.metadata()?.dev() == new_dir.metadata()?.dev())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let old_dir = canonicalize(old_dir)?;
|
||||
let new_dir = canonicalize(new_dir)?;
|
||||
|
||||
let old_component = old_dir.components().next();
|
||||
let new_component = new_dir.components().next();
|
||||
|
||||
match (old_component, new_component) {
|
||||
(
|
||||
Some(std::path::Component::Prefix(old)),
|
||||
Some(std::path::Component::Prefix(new)),
|
||||
) => Ok(old.as_os_str() == new.as_os_str()),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rename
|
||||
pub async fn rename(
|
||||
pub async fn rename_or_move(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
to: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
tokio::fs::rename(from, to)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: from.to_string_lossy().to_string(),
|
||||
})
|
||||
|
||||
if to
|
||||
.parent()
|
||||
.map_or(Ok(false), |to_dir| is_same_disk(from, to_dir))?
|
||||
{
|
||||
tokio::fs::rename(from, to)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: from.to_string_lossy().to_string(),
|
||||
})
|
||||
} else {
|
||||
move_recursive(from, to).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn move_recursive(from: &Path, to: &Path) -> Result<(), IOError> {
|
||||
if from.is_file() {
|
||||
copy(from, to).await?;
|
||||
remove_file(from).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
create_dir(to).await?;
|
||||
|
||||
let mut dir = read_dir(from).await?;
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let new_path = to.join(entry.file_name());
|
||||
move_recursive(&entry.path(), &new_path).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// copy
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ariadne"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde"] }
|
||||
serde_bytes = "0.11"
|
||||
rand = "0.8.5"
|
||||
either = "1.13"
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
serde_cbor = "0.11"
|
||||
@@ -0,0 +1,216 @@
|
||||
pub use super::users::UserId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62.
|
||||
///
|
||||
/// Uses `rand`'s thread rng on every call.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
#[inline]
|
||||
pub fn random_base62(n: usize) -> u64 {
|
||||
random_base62_rng(&mut rand::thread_rng(), n)
|
||||
}
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62, using the given rng.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
|
||||
random_base62_rng_range(rng, n, n)
|
||||
}
|
||||
|
||||
pub fn random_base62_rng_range<R: rand::RngCore>(
|
||||
rng: &mut R,
|
||||
n_min: usize,
|
||||
n_max: usize,
|
||||
) -> u64 {
|
||||
use rand::Rng;
|
||||
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// which is n characters long when encoded
|
||||
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
|
||||
}
|
||||
|
||||
const MULTIPLES: [u64; 12] = [
|
||||
1,
|
||||
62,
|
||||
62 * 62,
|
||||
62 * 62 * 62,
|
||||
62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
u64::MAX,
|
||||
];
|
||||
|
||||
/// An ID encoded as base62 for use in the API.
|
||||
///
|
||||
/// All ids should be random and encode to 8-10 character base62 strings,
|
||||
/// to avoid enumeration and other attacks.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Base62Id(pub u64);
|
||||
|
||||
/// An error decoding a number from base62.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecodingError {
|
||||
/// Encountered a non-base62 character in a base62 string
|
||||
#[error("Invalid character {0:?} in base62 encoding")]
|
||||
InvalidBase62(char),
|
||||
/// Encountered integer overflow when decoding a base62 id.
|
||||
#[error("Base62 decoding overflowed")]
|
||||
Overflow,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! from_base62id {
|
||||
($($struct:ty, $con:expr;)+) => {
|
||||
$(
|
||||
impl From<Base62Id> for $struct {
|
||||
fn from(id: Base62Id) -> $struct {
|
||||
$con(id.0)
|
||||
}
|
||||
}
|
||||
impl From<$struct> for Base62Id {
|
||||
fn from(id: $struct) -> Base62Id {
|
||||
Base62Id(id.0)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_base62_display {
|
||||
($struct:ty) => {
|
||||
impl std::fmt::Display for $struct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&$crate::ids::base62_impl::to_base62(self.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_base62_display!(Base62Id);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base62_id_impl {
|
||||
($struct:ty, $cons:expr) => {
|
||||
$crate::ids::from_base62id!($struct, $cons;);
|
||||
$crate::ids::impl_base62_display!($struct);
|
||||
}
|
||||
}
|
||||
base62_id_impl!(UserId, UserId);
|
||||
|
||||
pub use {base62_id_impl, from_base62id, impl_base62_display};
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
use serde::ser::Serializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Base62Id, DecodingError};
|
||||
|
||||
impl<'de> Deserialize<'de> for Base62Id {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Base62Visitor;
|
||||
|
||||
impl Visitor<'_> for Base62Visitor {
|
||||
type Value = Base62Id;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
formatter.write_str("a base62 string id")
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Base62Id(v))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
parse_base62(string).map(Base62Id).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
if deserializer.is_human_readable() {
|
||||
deserializer.deserialize_str(Base62Visitor)
|
||||
} else {
|
||||
deserializer.deserialize_u64(Base62Visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Base62Id {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if serializer.is_human_readable() {
|
||||
serializer.serialize_str(&to_base62(self.0))
|
||||
} else {
|
||||
serializer.serialize_u64(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BASE62_CHARS: [u8; 62] =
|
||||
*b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
pub fn to_base62(mut num: u64) -> String {
|
||||
let length = (num as f64).log(62.0).ceil() as usize;
|
||||
let mut output = String::with_capacity(length);
|
||||
|
||||
while num > 0 {
|
||||
// Could be done more efficiently, but requires byte
|
||||
// manipulation of strings & Vec<u8> -> String conversion
|
||||
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
|
||||
num /= 62;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||
let mut num: u64 = 0;
|
||||
for c in string.chars() {
|
||||
let next_digit;
|
||||
if c.is_ascii_digit() {
|
||||
next_digit = (c as u8 - b'0') as u64;
|
||||
} else if c.is_ascii_uppercase() {
|
||||
next_digit = 10 + (c as u8 - b'A') as u64;
|
||||
} else if c.is_ascii_lowercase() {
|
||||
next_digit = 36 + (c as u8 - b'a') as u64;
|
||||
} else {
|
||||
return Err(DecodingError::InvalidBase62(c));
|
||||
}
|
||||
|
||||
// We don't want this panicking or wrapping on integer overflow
|
||||
if let Some(n) =
|
||||
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
|
||||
{
|
||||
num = n;
|
||||
} else {
|
||||
return Err(DecodingError::Overflow);
|
||||
}
|
||||
}
|
||||
Ok(num)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod ids;
|
||||
pub mod networking;
|
||||
pub mod users;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod ids;
|
||||
pub mod networking;
|
||||
pub mod users;
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::ids::UserId;
|
||||
use crate::users::UserStatus;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientToServerMessage {
|
||||
StatusUpdate {
|
||||
profile_name: Option<String>,
|
||||
},
|
||||
|
||||
SocketListen {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketClose {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketSend {
|
||||
socket: Uuid,
|
||||
#[serde(with = "serde_bytes")]
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerToClientMessage {
|
||||
StatusUpdate {
|
||||
status: UserStatus,
|
||||
},
|
||||
UserOffline {
|
||||
id: UserId,
|
||||
},
|
||||
FriendStatuses {
|
||||
statuses: Vec<UserStatus>,
|
||||
},
|
||||
FriendRequest {
|
||||
from: UserId,
|
||||
},
|
||||
FriendRequestRejected {
|
||||
from: UserId,
|
||||
},
|
||||
|
||||
FriendSocketListening {
|
||||
user: UserId,
|
||||
socket: Uuid,
|
||||
},
|
||||
FriendSocketStoppedListening {
|
||||
user: UserId,
|
||||
},
|
||||
|
||||
SocketConnected {
|
||||
to_socket: Uuid,
|
||||
new_socket: Uuid,
|
||||
},
|
||||
SocketClosed {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketData {
|
||||
socket: Uuid,
|
||||
#[serde(with = "serde_bytes")]
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod message;
|
||||
pub mod serialization;
|
||||
@@ -0,0 +1,56 @@
|
||||
use super::message::{ClientToServerMessage, ServerToClientMessage};
|
||||
use either::Either;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SerializationError {
|
||||
#[error("Failed to (de)serialize message: {0}")]
|
||||
SerializationFailed(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to (de)serialize binary message: {0}")]
|
||||
BinarySerializationFailed(#[from] serde_cbor::Error),
|
||||
}
|
||||
|
||||
macro_rules! message_serialization {
|
||||
($message_enum:ty $(,$binary_pattern:pat_param)* $(,)?) => {
|
||||
impl $message_enum {
|
||||
pub fn is_binary(&self) -> bool {
|
||||
match self {
|
||||
$(
|
||||
$binary_pattern => true,
|
||||
)*
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(
|
||||
&self,
|
||||
) -> Result<Either<String, Vec<u8>>, SerializationError> {
|
||||
Ok(match self {
|
||||
$(
|
||||
$binary_pattern => Either::Right(serde_cbor::to_vec(self)?),
|
||||
)*
|
||||
_ => Either::Left(serde_json::to_string(self)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
msg: Either<&str, &[u8]>,
|
||||
) -> Result<Self, SerializationError> {
|
||||
Ok(match msg {
|
||||
Either::Left(text) => serde_json::from_str(&text)?,
|
||||
Either::Right(bytes) => serde_cbor::from_slice(&bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
message_serialization!(
|
||||
ClientToServerMessage,
|
||||
ClientToServerMessage::SocketSend { .. },
|
||||
);
|
||||
message_serialization!(
|
||||
ServerToClientMessage,
|
||||
ServerToClientMessage::SocketData { .. },
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
use super::ids::Base62Id;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct UserId(pub u64);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserStatus {
|
||||
pub user_id: UserId,
|
||||
pub profile_name: Option<String>,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M10 21.8c-1.3-.3-2.4-.7-3.5-1.5M17.6 3.7c1.1.7 2 1.6 2.7 2.7M2.2 10c.3-1.3.7-2.4 1.5-3.5M20.3 17.6c-.7 1.1-1.6 2-2.7 2.7M21.8 10.1c.2 1.3.2 2.5 0 3.8M6.5 3.6c1.1-.7 2.3-1.2 3.5-1.5M3.6 17.5c-.7-1.1-1.2-2.3-1.5-3.5"/>
|
||||
<path d="M13.9 2.2c4.6.9 8.1 5 8.1 9.8s-3.4 8.9-8 9.8"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
@@ -105,6 +105,7 @@ import _HeartHandshakeIcon from './icons/heart-handshake.svg?component'
|
||||
import _HistoryIcon from './icons/history.svg?component'
|
||||
import _HomeIcon from './icons/home.svg?component'
|
||||
import _ImageIcon from './icons/image.svg?component'
|
||||
import _InProgressIcon from './icons/in-progress.svg?component'
|
||||
import _InfoIcon from './icons/info.svg?component'
|
||||
import _IssuesIcon from './icons/issues.svg?component'
|
||||
import _KeyIcon from './icons/key.svg?component'
|
||||
@@ -390,6 +391,7 @@ export const HeartHandshakeIcon = _HeartHandshakeIcon
|
||||
export const HistoryIcon = _HistoryIcon
|
||||
export const HomeIcon = _HomeIcon
|
||||
export const ImageIcon = _ImageIcon
|
||||
export const InProgressIcon = _InProgressIcon
|
||||
export const InfoIcon = _InfoIcon
|
||||
export const IssuesIcon = _IssuesIcon
|
||||
export const KeyIcon = _KeyIcon
|
||||
|
||||
@@ -709,7 +709,7 @@ const customNotAllowedToContinue = computed(
|
||||
(!customMatchingProduct.value || customLoading.value || customOutOfStock.value),
|
||||
)
|
||||
const upgradeNotAllowedToContinue = computed(
|
||||
() => props.existingSubscription && (!hasUpgradeCapacityForConfig.value || customLoading.value),
|
||||
() => props.existingSubscription && (customOutOfStock.value || customLoading.value),
|
||||
)
|
||||
|
||||
const customServerConfig = reactive({
|
||||
@@ -1009,11 +1009,6 @@ async function submitPayment() {
|
||||
paymentLoading.value = false
|
||||
}
|
||||
|
||||
const hasUpgradeCapacityForConfig = computed(() => {
|
||||
// TODO: Check for upgrade capacity here when Pyro provides route
|
||||
return props.existingPlan
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
if (props.customServer) {
|
||||
|
||||
@@ -10,6 +10,27 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2025-03-02T18:45:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Added option to copy version IDs from the version list for project members and developer mode.
|
||||
- Fixed the staff moderation checklist going off the screen.`,
|
||||
},
|
||||
{
|
||||
date: `2025-02-25T10:20:00-08:00`,
|
||||
product: 'servers',
|
||||
body: `### Improvements
|
||||
- Fixed server upgrades being allowed when out of stock, despite warning.`,
|
||||
},
|
||||
{
|
||||
date: `2025-02-25T10:20:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Moved Minecraft brand disclaimer to bottom of footer.
|
||||
- Improved clarity of the ongoing revenue period footnote on the Revenue page.
|
||||
- Fixed collections without a summary being unable to be edited.`,
|
||||
},
|
||||
{
|
||||
date: `2025-02-21T13:30:00-08:00`,
|
||||
product: 'web',
|
||||
|
||||
Reference in new issue
Block a user