mirror of
https://github.com/OMGeeky/gdriver2.git
synced 2026-01-01 01:09:57 +01:00
some tracing & general restructure
This commit is contained in:
@@ -6,13 +6,16 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
fuser={ version = "0.14", default_features = true, features = ["serializable"] }
|
||||
tracing.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
tarpc.workspace = true
|
||||
futures.workspace = true
|
||||
chrono.workspace = true
|
||||
lazy_static = "1.4.0"
|
||||
thiserror = "1.0.56"
|
||||
google-drive3 = "5.0.4"
|
||||
const_format = "0.2"
|
||||
|
||||
[dependencies.gdriver-common]
|
||||
path = "../gdriver-common/"
|
||||
path = "../gdriver-common"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::prelude::*;
|
||||
use futures::{future, prelude::*};
|
||||
use std::net::SocketAddr;
|
||||
use tarpc::{
|
||||
@@ -5,10 +6,9 @@ use tarpc::{
|
||||
server::{self, incoming::Incoming, Channel},
|
||||
tokio_serde::formats::Json,
|
||||
};
|
||||
mod prelude;
|
||||
use crate::prelude::*;
|
||||
pub(crate) use gdriver_common::prelude::*;
|
||||
|
||||
mod drive;
|
||||
mod prelude;
|
||||
mod sample;
|
||||
mod service;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
||||
}
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
gdriver_common::tracing_setup::init_tracing();
|
||||
// sample::main().await?;
|
||||
service::start().await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
use super::*;
|
||||
use crate::drive::Drive;
|
||||
use chrono::Duration;
|
||||
use gdriver_common::{
|
||||
drive_structure::drive_id::{DriveId, ROOT_ID},
|
||||
ipc::gdriver_service::{*, errors::*},
|
||||
|
||||
ipc::gdriver_service::{errors::*, *},
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc, thread};
|
||||
use std::ffi::OsString;
|
||||
use std::{path::PathBuf, sync::Arc, thread};
|
||||
use tarpc::context::Context;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::drive::Drive;
|
||||
|
||||
use super::*;
|
||||
#[derive(Clone)]
|
||||
struct GdriverServer {
|
||||
socket_address: SocketAddr,
|
||||
drive: Arc<Mutex<Drive>>,
|
||||
}
|
||||
impl GDriverService for GdriverServer {
|
||||
async fn get_settings(self, context: Context) -> StdResult<GDriverSettings, GetSettingsError> {
|
||||
todo!()
|
||||
}
|
||||
// async fn get_settings(self, context: Context) -> StdResult<GDriverSettings, GetSettingsError> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
async fn get_file_by_name(self, context: Context, name: OsString, parent: DriveId) -> StdResult<DriveId, GetFileByPathError> {
|
||||
todo!()
|
||||
@@ -62,8 +60,17 @@ impl GDriverService for GdriverServer {
|
||||
self,
|
||||
context: Context,
|
||||
id: DriveId,
|
||||
) -> StdResult<(), GetFileListError> {
|
||||
todo!()
|
||||
) -> StdResult<Vec<ReadDirResult>, GetFileListError> {
|
||||
Err(GetFileListError::Other)
|
||||
}
|
||||
|
||||
async fn list_files_in_directory_with_offset(
|
||||
self,
|
||||
context: Context,
|
||||
id: DriveId,
|
||||
offset: u64,
|
||||
) -> StdResult<Vec<ReadDirResult>, GetFileListError> {
|
||||
Err(GetFileListError::Other)
|
||||
}
|
||||
|
||||
async fn mark_file_as_deleted(
|
||||
@@ -99,11 +106,21 @@ impl GDriverService for GdriverServer {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn update_changes(
|
||||
self,
|
||||
context: Context,
|
||||
) -> StdResult<(), UpdateChangesError> {
|
||||
todo!()
|
||||
async fn update_changes(self, context: Context) -> StdResult<(), UpdateChangesError> {
|
||||
let drive = self.drive.try_lock();
|
||||
match drive {
|
||||
Ok(mut drive) => {
|
||||
drive.update().await.map_err(|e| {
|
||||
info!("Error while updating: {e}");
|
||||
dbg!(e);
|
||||
UpdateChangesError::Remote
|
||||
})?;
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(UpdateChangesError::Running);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_something2(
|
||||
@@ -111,32 +128,32 @@ impl GDriverService for GdriverServer {
|
||||
_: Context,
|
||||
req: BackendActionRequest,
|
||||
) -> std::result::Result<String, BackendActionError> {
|
||||
println!("You are connected from {}", self.socket_address);
|
||||
info!("You are connected from {}", self.socket_address);
|
||||
|
||||
match req {
|
||||
BackendActionRequest::ShutdownGracefully => {
|
||||
println!("Shutdown request received, but I dont want to.");
|
||||
info!("Shutdown request received, but I dont want to.");
|
||||
Err(BackendActionError::CouldNotComplete)
|
||||
//Ok(String::from("OK. Shutting down"))
|
||||
}
|
||||
BackendActionRequest::UpdateChanges => {
|
||||
println!("UpdateChanges request received");
|
||||
info!("UpdateChanges request received");
|
||||
let drive = &self.drive;
|
||||
print_sample_tracking_state(drive).await;
|
||||
|
||||
Ok(String::from("OK"))
|
||||
}
|
||||
BackendActionRequest::Ping => {
|
||||
println!("Ping request received");
|
||||
info!("Ping request received");
|
||||
Ok(String::from("Pong"))
|
||||
}
|
||||
BackendActionRequest::RunLong => {
|
||||
println!("RunLong request received");
|
||||
info!("RunLong request received");
|
||||
long_running_task(&self.drive).await;
|
||||
Ok(String::from("OK"))
|
||||
}
|
||||
BackendActionRequest::StartLong => {
|
||||
println!("StartLong request received");
|
||||
info!("StartLong request received");
|
||||
tokio::spawn(async move { long_running_task(&self.drive).await });
|
||||
Ok(String::from("OK"))
|
||||
}
|
||||
@@ -153,9 +170,9 @@ async fn print_sample_tracking_state(drive: &Arc<Mutex<Drive>>) {
|
||||
dbg!(state);
|
||||
}
|
||||
pub async fn start() -> Result<()> {
|
||||
println!("Hello, world!");
|
||||
info!("Hello, world!");
|
||||
let config = &CONFIGURATION;
|
||||
println!("Config: {:?}", **config);
|
||||
info!("Config: {:?}", **config);
|
||||
|
||||
let drive = Drive::new();
|
||||
let m = Arc::new(Mutex::new(drive));
|
||||
@@ -164,7 +181,7 @@ pub async fn start() -> Result<()> {
|
||||
let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default).await?;
|
||||
listener.config_mut().max_frame_length(usize::MAX);
|
||||
|
||||
println!("Listening");
|
||||
info!("Listening");
|
||||
listener
|
||||
// Ignore accept errors.
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
|
||||
@@ -9,7 +9,7 @@ use tarpc::serde::{Deserialize, Serialize};
|
||||
|
||||
type Inode = u64;
|
||||
const BLOCK_SIZE: u64 = 512;
|
||||
trait ConvertFileType {
|
||||
pub trait ConvertFileType {
|
||||
fn from_ft(kind: FileType) -> Self;
|
||||
fn into_ft(self) -> FileType;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
use std::time;
|
||||
|
||||
use gdriver_common::ipc::gdriver_service::{BackendActionRequest, GDriverServiceClient};
|
||||
@@ -14,14 +13,14 @@ pub async fn start() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ping(client: &GDriverServiceClient) -> Result<()>{
|
||||
async fn ping(client: &GDriverServiceClient) -> Result<()> {
|
||||
let hello = client
|
||||
.do_something2(tarpc::context::current(), BackendActionRequest::Ping)
|
||||
.await;
|
||||
match hello {
|
||||
Ok(hello) => println!("Yay: {:?}", hello),
|
||||
Ok(hello) => info!("Yay: {:?}", hello),
|
||||
Err(e) => {
|
||||
println!(":( {:?}", (e));
|
||||
error!(":( {:?}", (e));
|
||||
dbg!(&e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
@@ -40,8 +39,8 @@ async fn run_long_stuff_test(client: &GDriverServiceClient) {
|
||||
.as_secs();
|
||||
|
||||
match hello {
|
||||
Ok(hello) => println!("Run Long returned after {} seconds: {:?}", seconds, hello),
|
||||
Err(e) => println!(":( {:?}", (e)),
|
||||
Ok(hello) => info!("Run Long returned after {} seconds: {:?}", seconds, hello),
|
||||
Err(e) => error!(":( {:?}", (e)),
|
||||
}
|
||||
let start = time::SystemTime::now();
|
||||
let hello = client
|
||||
@@ -52,8 +51,8 @@ async fn run_long_stuff_test(client: &GDriverServiceClient) {
|
||||
.as_secs();
|
||||
|
||||
match hello {
|
||||
Ok(hello) => println!("Start Long returned after {} seconds: {:?}", seconds, hello),
|
||||
Err(e) => println!(":( {:?}", (e)),
|
||||
Ok(hello) => info!("Start Long returned after {} seconds: {:?}", seconds, hello),
|
||||
Err(e) => info!(":( {:?}", (e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ pub async fn create_client(ip: IpAddr, port: u16) -> Result<GDriverServiceClient
|
||||
let transport = tarpc::serde_transport::tcp::connect(&server_addr, Json::default)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
println!("Could not connect to backend. Please make sure it is started before this app.");
|
||||
info!("Could not connect to backend. Please make sure it is started before this app.");
|
||||
e
|
||||
})?;
|
||||
let service = GDriverServiceClient::new(client::Config::default(), transport);
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod drive_id;
|
||||
pub mod meta;
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub struct DriveObject {
|
||||
pub id: drive_id::DriveId,
|
||||
pub metadata: meta::Metadata,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter};
|
||||
lazy_static! {
|
||||
pub static ref ROOT_ID: DriveId = DriveId(String::from("root"));
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub struct DriveId(pub String);
|
||||
|
||||
impl<T> From<T> for DriveId
|
||||
|
||||
@@ -3,8 +3,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
pub type TIMESTAMP = (i64, u32);
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub struct Metadata {
|
||||
pub state: FileState,
|
||||
pub size: u64,
|
||||
@@ -26,14 +28,14 @@ pub fn write_metadata_file(path: &Path, metadata: &Metadata) -> Result<()> {
|
||||
let reader = File::open(path)?;
|
||||
Ok(serde_json::to_writer(reader, metadata)?)
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Debug)]
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub enum FileState {
|
||||
Downloaded,
|
||||
Cached,
|
||||
MetadataOnly,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Debug)]
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub enum FileKind {
|
||||
File,
|
||||
Directory,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub mod gdriver_service;
|
||||
pub mod gdriver_settings;
|
||||
pub mod sample;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::prelude::*;
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::drive_structure::drive_id::DriveId;
|
||||
use crate::drive_structure::meta::FileKind;
|
||||
use crate::ipc::gdriver_settings::GDriverSettings;
|
||||
use crate::prelude::*;
|
||||
use errors::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait GDriverService {
|
||||
async fn get_settings() -> StdResult<GDriverSettings, GetSettingsError>;
|
||||
async fn get_file_by_name(
|
||||
name: OsString,
|
||||
parent: DriveId,
|
||||
@@ -17,7 +18,13 @@ pub trait GDriverService {
|
||||
async fn write_local_change(id: DriveId) -> StdResult<(), WriteLocalChangeError>;
|
||||
async fn get_metadata_for_file(id: DriveId) -> StdResult<(), GetMetadataError>;
|
||||
async fn download_content_for_file(id: DriveId) -> StdResult<(), GetContentError>;
|
||||
async fn list_files_in_directory(id: DriveId) -> StdResult<(), GetFileListError>;
|
||||
async fn list_files_in_directory(
|
||||
id: DriveId,
|
||||
) -> StdResult<Vec<ReadDirResult>, GetFileListError>;
|
||||
async fn list_files_in_directory_with_offset(
|
||||
id: DriveId,
|
||||
offset: u64,
|
||||
) -> StdResult<Vec<ReadDirResult>, GetFileListError>;
|
||||
async fn mark_file_as_deleted(id: DriveId) -> StdResult<(), MarkFileAsDeletedError>;
|
||||
async fn mark_file_for_keeping_local(
|
||||
id: DriveId,
|
||||
@@ -39,49 +46,10 @@ pub enum BackendActionRequest {
|
||||
StartLong,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GDriverSettings {
|
||||
metadata_path: PathBuf,
|
||||
cache_path: PathBuf,
|
||||
downloaded_path: PathBuf,
|
||||
}
|
||||
impl GDriverSettings {
|
||||
pub fn metadata_path(&self) -> &Path {
|
||||
&self.metadata_path
|
||||
}
|
||||
pub fn cache_path(&self) -> &Path {
|
||||
&self.cache_path
|
||||
}
|
||||
pub fn downloaded_path(&self) -> &Path {
|
||||
&self.downloaded_path
|
||||
}
|
||||
|
||||
pub fn get_metadata_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.metadata_path.join(id.as_ref()).with_extension("meta")
|
||||
}
|
||||
pub fn get_downloaded_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.downloaded_path.join(id.as_ref())
|
||||
}
|
||||
pub fn get_cache_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.cache_path.join(id.as_ref())
|
||||
}
|
||||
lazy_static! {
|
||||
pub static ref SETTINGS: GDriverSettings = GDriverSettings::default();
|
||||
}
|
||||
|
||||
impl Default for GDriverSettings {
|
||||
fn default() -> Self {
|
||||
let p = directories::ProjectDirs::from("com", "OMGeeky", "gdriver2").expect(
|
||||
"Getting the Project dir needs to work (on all platforms) otherwise nothing will work as expected. \
|
||||
This is where all files will be stored, so there is not much use for this app without it.",
|
||||
);
|
||||
Self {
|
||||
metadata_path: p.data_dir().join("meta"),
|
||||
downloaded_path: p.data_dir().join("downloads"),
|
||||
cache_path: p.cache_dir().to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use errors::*;
|
||||
pub mod errors {
|
||||
use super::*;
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
@@ -125,10 +93,26 @@ pub mod errors {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum GetFileByPathError {}
|
||||
pub enum GetFileByPathError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
#[error("The Specified name is invalid")]
|
||||
InvalidName,
|
||||
#[error("Could not find name specified")]
|
||||
NotFound,
|
||||
#[error("Could not update drive info")]
|
||||
Update(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum UpdateChangesError {}
|
||||
pub enum UpdateChangesError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
#[error("Remote error")]
|
||||
Remote,
|
||||
#[error("Already running")]
|
||||
Running,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum WriteLocalChangeError {
|
||||
@@ -143,22 +127,47 @@ pub mod errors {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum GetMetadataError {}
|
||||
pub enum GetMetadataError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum GetContentError {}
|
||||
pub enum GetContentError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
|
||||
//#[derive(Debug, Serialize, Deserialize)]
|
||||
//pub enum GetContentError {}
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum GetFileListError {}
|
||||
pub enum GetFileListError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum MarkFileAsDeletedError {}
|
||||
pub enum MarkFileAsDeletedError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum MarkFileForKeepingLocalError {}
|
||||
pub enum MarkFileForKeepingLocalError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum UnmarkFileForKeepingLocalError {}
|
||||
pub enum UnmarkFileForKeepingLocalError {
|
||||
#[error("Other")]
|
||||
Other,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Clone, Hash)]
|
||||
pub struct ReadDirResult {
|
||||
pub id: DriveId,
|
||||
pub kind: FileKind,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
53
gdriver-common/src/ipc/gdriver_settings.rs
Normal file
53
gdriver-common/src/ipc/gdriver_settings.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::prelude::DriveId;
|
||||
use crate::project_dirs::PROJECT_DIRS;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GDriverSettings {
|
||||
metadata_path: PathBuf,
|
||||
cache_path: PathBuf,
|
||||
downloaded_path: PathBuf,
|
||||
data_path: PathBuf,
|
||||
}
|
||||
|
||||
impl GDriverSettings {
|
||||
pub fn metadata_path(&self) -> &Path {
|
||||
&self.metadata_path
|
||||
}
|
||||
pub fn cache_path(&self) -> &Path {
|
||||
&self.cache_path
|
||||
}
|
||||
pub fn downloaded_path(&self) -> &Path {
|
||||
&self.downloaded_path
|
||||
}
|
||||
pub fn data_path(&self) -> &Path {
|
||||
&self.data_path
|
||||
}
|
||||
|
||||
pub fn get_changes_file_path(&self) -> PathBuf {
|
||||
self.data_path.join("changes.txt")
|
||||
}
|
||||
|
||||
pub fn get_metadata_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.metadata_path.join(id.as_ref()).with_extension("meta")
|
||||
}
|
||||
pub fn get_downloaded_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.downloaded_path.join(id.as_ref())
|
||||
}
|
||||
pub fn get_cache_file_path(&self, id: &DriveId) -> PathBuf {
|
||||
self.cache_path.join(id.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GDriverSettings {
|
||||
fn default() -> Self {
|
||||
let p = &PROJECT_DIRS;
|
||||
Self {
|
||||
metadata_path: p.data_dir().join("meta"),
|
||||
downloaded_path: p.data_dir().join("downloads"),
|
||||
cache_path: p.cache_dir().to_path_buf(),
|
||||
data_path: p.data_dir().join("data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ pub mod prelude;
|
||||
pub mod config;
|
||||
pub mod drive_structure;
|
||||
pub mod ipc;
|
||||
pub mod project_dirs;
|
||||
pub mod tracing_setup;
|
||||
|
||||
7
gdriver-common/src/project_dirs.rs
Normal file
7
gdriver-common/src/project_dirs.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static!(
|
||||
pub static ref PROJECT_DIRS: directories::ProjectDirs = directories::ProjectDirs::from("com", "OMGeeky", "gdriver2").expect(
|
||||
"Getting the Project dir needs to work (on all platforms) otherwise nothing will work as expected. \
|
||||
This is where all files will be stored, so there is not much use for this app without it.",
|
||||
);
|
||||
);
|
||||
Reference in New Issue
Block a user