Add bare config file
This commit is contained in:
Generated
+3734
-3
File diff suppressed because it is too large
Load Diff
+18
-1
@@ -1,9 +1,26 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imojefs"
|
name = "imojefs"
|
||||||
description = "Johnson & Johnson IMoje File Sync"
|
description = "Johnson & Johnson IMoje CLI"
|
||||||
authors = ["Wim Velzeboer <wim@velzeboer.nl>"]
|
authors = ["Wim Velzeboer <wim@velzeboer.nl>"]
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.104"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
|
||||||
|
aws-credential-types = "1.2.14"
|
||||||
|
aws-sdk-s3 = "1.132.0"
|
||||||
|
base64 = "0.22"
|
||||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||||
|
dirs = "6.0"
|
||||||
|
env_logger = "0.11.10"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
log = "0.4"
|
||||||
|
rand = "0.8"
|
||||||
|
rustsf = { path = "../rustsf" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
signal-hook = "0.4.4"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
toml = "0.9"
|
||||||
|
|||||||
@@ -8,3 +8,11 @@
|
|||||||
|___\____|__ /\____/\__| |\___ >___ / /_______ /
|
|___\____|__ /\____/\__| |\___ >___ / /_______ /
|
||||||
\/ \______| \/ \/ \/
|
\/ \______| \/ \/ \/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
This CLI tool extracts binary file contents from Salesforce to S3.
|
||||||
|
It leave a link with the S3 object-key in Salesforce,
|
||||||
|
so that a pre-signed url can be generated whenever needed.
|
||||||
|
|
||||||
|
The tool is deigned to run at certain intervals,
|
||||||
|
e.g. twice daily, and should be scheduled in the standard crontab in Linux
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use aws_config::Region;
|
||||||
|
|
||||||
|
use aws_credential_types::provider::SharedCredentialsProvider;
|
||||||
|
|
||||||
|
pub(crate) async fn connect(
|
||||||
|
client_id: &str,
|
||||||
|
client_secret: &str,
|
||||||
|
custom_endpoint: Option<&str>,
|
||||||
|
region: &str) -> aws_sdk_s3::Client {
|
||||||
|
|
||||||
|
let creds = SharedCredentialsProvider::new(aws_credential_types::Credentials::new(
|
||||||
|
client_id, client_secret, None, None, "Static",
|
||||||
|
));
|
||||||
|
|
||||||
|
let config = match custom_endpoint {
|
||||||
|
Some(endpoint) => {
|
||||||
|
aws_config::from_env()
|
||||||
|
.region(Region::new(region.to_string()))
|
||||||
|
.endpoint_url(endpoint)
|
||||||
|
.credentials_provider(creds)
|
||||||
|
.load()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
aws_config::from_env()
|
||||||
|
.region(Region::new(region.to_string()))
|
||||||
|
.credentials_provider(creds)
|
||||||
|
.load()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
aws_sdk_s3::Client::new(&config)
|
||||||
|
}
|
||||||
+141
-38
@@ -1,49 +1,122 @@
|
|||||||
|
use crate::logger;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
/// Number of seconds to wait
|
||||||
|
#[arg(short = 'w', long, default_value_t = 20, env = "IMOJEFS_WAIT")]
|
||||||
|
pub wait: u64,
|
||||||
|
|
||||||
|
/// The logging level
|
||||||
|
#[arg(short = 'l', long, value_enum, default_value_t = LogLevel::Info, env = "IMOJEFS_LOG_LEVEL")]
|
||||||
|
pub log_level: LogLevel,
|
||||||
|
|
||||||
|
/// The configuration to use
|
||||||
|
#[arg(short = 'c', long, default_value_t = String::from("imoje"))]
|
||||||
|
config: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug)]
|
||||||
|
enum Commands {
|
||||||
|
/// Controls testing features
|
||||||
|
FileSync,
|
||||||
|
|
||||||
|
/// Change Settings
|
||||||
|
Set {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<SettingsCommands>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug)]
|
||||||
|
enum SettingsCommands {
|
||||||
|
/// Set the Salesforce Authentication Url
|
||||||
|
SfdxAuthUrl {
|
||||||
|
/// Salesforce Authentication Url
|
||||||
|
#[arg(
|
||||||
|
short = 'x',
|
||||||
|
long,
|
||||||
|
env = "IMOJEEH_SFDX_AUTH_URL",
|
||||||
|
hide_env_values = true
|
||||||
|
)]
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
/// AWS Bucket name
|
||||||
|
#[arg(short = 'b', long, env = "IMOJEFS_S3_BUCKET")]
|
||||||
|
pub s3_bucket: String,
|
||||||
|
|
||||||
/// AWS Bucket name
|
/// AWS Access Key Id
|
||||||
#[arg(short = 'b', long, env = "IMOJEFS_S3_BUCKET")]
|
#[arg(short = 'k', long, env = "IMOJEFS_S3_CLIENT_ID")]
|
||||||
pub s3_bucket: String,
|
pub s3_client_id: String,
|
||||||
|
|
||||||
/// AWS Access Key Id
|
/// AWS Secret Access Key
|
||||||
#[arg(short = 'k', long, env = "IMOJEFS_S3_CLIENT_ID")]
|
#[arg(
|
||||||
pub s3_key: String,
|
short = 's',
|
||||||
|
long,
|
||||||
|
env = "IMOJEFS_S3_CLIENT_SECRET",
|
||||||
|
hide_env_values = true
|
||||||
|
)]
|
||||||
|
pub s3_client_secret: String,
|
||||||
|
|
||||||
/// AWS Secret Access Key
|
/// AWS Endpoint URL, [default: AWS url]
|
||||||
#[arg(short = 's', long, env = "IMOJEFS_S3_SECRET", hide_env_values = true)]
|
#[arg(short = 'e', long, env = "IMOJEFS_S3_ENDPOINT")]
|
||||||
pub s3_secret: String,
|
pub s3_endpoint: Option<String>,
|
||||||
|
|
||||||
/// AWS Endpoint URL, [default: AWS url]
|
/// AWS Region
|
||||||
#[arg(short = 'e', long, env = "IMOJEFS_S3_ENDPOINT")]
|
#[arg(
|
||||||
pub s3_endpoint: Option<String>,
|
short = 'r',
|
||||||
|
long,
|
||||||
|
default_value = "us-east-1",
|
||||||
|
env = "IMOJEFS_S3_REGION"
|
||||||
|
)]
|
||||||
|
pub s3_region: String,
|
||||||
|
|
||||||
/// AWS Region
|
/// Salesforce Authentication Url
|
||||||
#[arg(short = 'r', long, default_value = "us-east-1", env = "IMOJEFS_S3_REGION")]
|
#[arg(
|
||||||
pub s3_region: String,
|
short = 'x',
|
||||||
|
long,
|
||||||
|
env = "IMOJEFS_SFDX_AUTH_URL",
|
||||||
|
hide_env_values = true
|
||||||
|
)]
|
||||||
|
pub sfdx_auth_url: String,
|
||||||
|
|
||||||
/// Salesforce Authentication Url
|
/// Retry failures after X runs
|
||||||
#[arg(short = 'x', long, env = "IMOJEFS_SFDX_AUTH_URL", hide_env_values = true)]
|
#[arg(long, default_value_t = 180, env = "IMOJEFS_RETRY")]
|
||||||
pub sfdx_auth_url: String,
|
pub retry: u64,
|
||||||
|
|
||||||
/// Number of seconds to wait
|
/// A comma separated list of fallback email address,
|
||||||
#[arg(short = 'w', long, default_value_t = 20, env = "IMOJEFS_WAIT")]
|
/// used when Salesforce cannot be reached to deliver log messages
|
||||||
pub wait: u64,
|
#[arg(short = 'a', long, env = "IMOJEFS_ADMIN_EMAIL")]
|
||||||
|
pub admin_email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The logging level
|
impl Args {
|
||||||
#[arg(short = 'l', long, value_enum, default_value_t = LogLevel::Info, env = "IMOJEFS_LOG_LEVEL")]
|
pub(crate) fn s3_bucket(&self) -> &str {
|
||||||
pub log_level: LogLevel,
|
&self.s3_bucket
|
||||||
|
}
|
||||||
/// Retry failures after X runs
|
pub(crate) fn s3_client_id(&self) -> &str {
|
||||||
#[arg(long, default_value_t = 180, env = "IMOJEFS_RETRY")]
|
&self.s3_client_id
|
||||||
pub retry: u64,
|
}
|
||||||
|
pub(crate) fn s3_client_secret(&self) -> &str {
|
||||||
/// A comma separated list of fallback email address,
|
&self.s3_client_secret
|
||||||
/// used when Salesforce cannot be reached to deliver log messages
|
}
|
||||||
#[arg(short = 'a', long, env = "IMOJEFS_ADMIN_EMAIL")]
|
pub(crate) fn s3_endpoint(&self) -> Option<&str> {
|
||||||
pub admin_email: Option<String>,
|
self.s3_endpoint.as_deref()
|
||||||
|
}
|
||||||
|
pub(crate) fn s3_region(&self) -> &str {
|
||||||
|
&self.s3_region
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the different levels of logging that can be used in an application.
|
/// Represents the different levels of logging that can be used in an application.
|
||||||
@@ -58,9 +131,39 @@ pub struct Args {
|
|||||||
/// as well as `Clone` and `Debug` for efficient copying and debugging purposes.
|
/// as well as `Clone` and `Debug` for efficient copying and debugging purposes.
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
#[derive(ValueEnum, Clone, Debug)]
|
||||||
pub enum LogLevel {
|
pub enum LogLevel {
|
||||||
Debug,
|
Debug,
|
||||||
Info,
|
Info,
|
||||||
Warning,
|
Warning,
|
||||||
Error,
|
Error,
|
||||||
Trace,
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_configuration_file(config_file: &str) -> Result<()> {
|
||||||
|
let app_config =
|
||||||
|
crate::config::load(config_file).context("Failed to load application configuration")?;
|
||||||
|
crate::APP_CONFIG
|
||||||
|
.set(app_config)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Application configuration has already been initialized"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
logger::init(&cli.log_level);
|
||||||
|
match load_configuration_file(&cli.config) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error loading configuration file: {:#}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Some(subcommand) => match subcommand {
|
||||||
|
Commands::FileSync { .. } => {}
|
||||||
|
Commands::Set { .. } => {}
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use aes_gcm::aead::{Aead, KeyInit, OsRng};
|
||||||
|
use aes_gcm::{Aes256Gcm, Nonce};
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
|
use base64::Engine;
|
||||||
|
use rand::RngCore;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const CONFIG_KEY_ENV: &str = "IMOJE_CONFIG_KEY";
|
||||||
|
const CONFIG_DIR: &str = "config/imoje";
|
||||||
|
const NONCE_LEN: usize = 12;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub dummy_setting: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
dummy_setting: "dummy-value".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(config_file: &str) -> Result<AppConfig> {
|
||||||
|
let config_path = config_path(config_file)?;
|
||||||
|
|
||||||
|
if !config_path.exists() {
|
||||||
|
let default_config = AppConfig::default();
|
||||||
|
save_encrypted(&config_path, &default_config)
|
||||||
|
.context("Unable to create encrypted default configuration file")?;
|
||||||
|
|
||||||
|
return Ok(default_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let encrypted_config = fs::read_to_string(&config_path)
|
||||||
|
.with_context(|| format!("Unable to read configuration file {}", config_path.display()))?;
|
||||||
|
|
||||||
|
decrypt_config(&encrypted_config)
|
||||||
|
.with_context(|| format!("Unable to decrypt configuration file {}", config_path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_path(config_file: &str) -> Result<PathBuf> {
|
||||||
|
let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Unable to determine home directory"))?;
|
||||||
|
|
||||||
|
Ok(home_dir.join(CONFIG_DIR).join(format!("{}.conf", config_file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_encrypted(config_path: &PathBuf, config: &AppConfig) -> Result<()> {
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Unable to create configuration directory {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialized_config = toml::to_string_pretty(config)
|
||||||
|
.context("Unable to serialize default configuration")?;
|
||||||
|
|
||||||
|
let encrypted_config = encrypt_config(&serialized_config)
|
||||||
|
.context("Unable to encrypt default configuration")?;
|
||||||
|
|
||||||
|
fs::write(config_path, encrypted_config)
|
||||||
|
.with_context(|| format!("Unable to write configuration file {}", config_path.display()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_config(config: &str) -> Result<String> {
|
||||||
|
let key = config_key()?;
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
|
.map_err(|_| anyhow!("Invalid encryption key length"))?;
|
||||||
|
|
||||||
|
let mut nonce_bytes = [0_u8; NONCE_LEN];
|
||||||
|
OsRng.fill_bytes(&mut nonce_bytes);
|
||||||
|
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(Nonce::from_slice(&nonce_bytes), config.as_bytes())
|
||||||
|
.map_err(|error| anyhow!("Unable to encrypt configuration: {error}"))?;
|
||||||
|
|
||||||
|
let mut encrypted_payload = Vec::with_capacity(NONCE_LEN + ciphertext.len());
|
||||||
|
encrypted_payload.extend_from_slice(&nonce_bytes);
|
||||||
|
encrypted_payload.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
Ok(BASE64.encode(encrypted_payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_config(encrypted_config: &str) -> Result<AppConfig> {
|
||||||
|
let encrypted_payload = BASE64
|
||||||
|
.decode(encrypted_config.trim())
|
||||||
|
.context("Configuration file is not valid base64")?;
|
||||||
|
|
||||||
|
if encrypted_payload.len() <= NONCE_LEN {
|
||||||
|
return Err(anyhow!("Configuration file is too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = config_key()?;
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
|
.map_err(|_| anyhow!("Invalid encryption key length"))?;
|
||||||
|
|
||||||
|
let nonce = Nonce::from_slice(&encrypted_payload[..NONCE_LEN]);
|
||||||
|
let ciphertext = &encrypted_payload[NONCE_LEN..];
|
||||||
|
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(nonce, ciphertext)
|
||||||
|
.map_err(|error| anyhow!("Unable to decrypt configuration: {error}"))?;
|
||||||
|
|
||||||
|
let plaintext = String::from_utf8(plaintext)
|
||||||
|
.context("Configuration file does not contain valid UTF-8")?;
|
||||||
|
|
||||||
|
toml::from_str(&plaintext).context("Unable to parse decrypted configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_key() -> Result<[u8; 32]> {
|
||||||
|
let encoded_key = std::env::var(CONFIG_KEY_ENV)
|
||||||
|
.unwrap_or("ihDlaCqJqUGJ1am4dT9k/BHRG2UT+iK9IfYcPe0Db/I=".to_string());
|
||||||
|
// .with_context(|| format!("Missing required environment variable {CONFIG_KEY_ENV}"))?;
|
||||||
|
|
||||||
|
let key = BASE64
|
||||||
|
.decode(encoded_key.trim())
|
||||||
|
.with_context(|| format!("{CONFIG_KEY_ENV} must be a base64-encoded 32-byte key"))?;
|
||||||
|
|
||||||
|
key.try_into()
|
||||||
|
.map_err(|_| anyhow!("{CONFIG_KEY_ENV} must decode to exactly 32 bytes"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::cli::LogLevel;
|
||||||
|
use log::{LevelFilter, info};
|
||||||
|
|
||||||
|
pub(crate) fn init(log_level: &LogLevel) {
|
||||||
|
let filter_level = match log_level {
|
||||||
|
LogLevel::Info => LevelFilter::Info,
|
||||||
|
LogLevel::Debug => LevelFilter::Debug,
|
||||||
|
LogLevel::Warning => LevelFilter::Warn,
|
||||||
|
LogLevel::Error => LevelFilter::Error,
|
||||||
|
LogLevel::Trace => LevelFilter::Trace,
|
||||||
|
};
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(filter_level)
|
||||||
|
.format_target(false)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
welcome_message()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn welcome_message() {
|
||||||
|
let authors = match option_env!("CARGO_PKG_AUTHORS") {
|
||||||
|
Some(authors_str) => {
|
||||||
|
", by ".to_string() + &authors_str.split(':').collect::<Vec<&str>>().join(", ")
|
||||||
|
}
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
let description = env!("CARGO_PKG_DESCRIPTION");
|
||||||
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
info!("Starting {}, v{}{}", description, version, authors);
|
||||||
|
info!(r#".___ _____ __ _________ .____ .___"#);
|
||||||
|
info!(r#"| | / \ ____ |__| ____ \_ ___ \| | | |"#);
|
||||||
|
info!(r#"| |/ \ / \ / _ \ | |/ __ \/ \ \/| | | |"#);
|
||||||
|
info!(r#"| / Y ( <_> ) | \ ___/\ \___| |___| |"#);
|
||||||
|
info!(r#"|___\____|__ /\____/\__| |\___ >\______ /_______ \___|"#);
|
||||||
|
info!(r#") \/ \______| \/ \/ \/"#);
|
||||||
|
}
|
||||||
+46
-3
@@ -1,14 +1,57 @@
|
|||||||
//! # IMoje File Sync
|
//! # IMoje File Sync
|
||||||
//!
|
//!
|
||||||
//! Syncs the contents of a Salesforce File to S3
|
//! Syncs the contents of a Salesforce File to S3
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
mod cli;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
fn main() {
|
mod aws_s3;
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
mod logger;
|
||||||
|
mod salesforce;
|
||||||
|
mod terminator;
|
||||||
|
|
||||||
|
|
||||||
|
pub static APP_CONFIG: OnceLock<config::AppConfig> = OnceLock::new();
|
||||||
|
|
||||||
|
|
||||||
|
#[::tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run();
|
||||||
|
/*
|
||||||
let args = cli::Args::parse();
|
let args = cli::Args::parse();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
println!("Hello, world!");
|
|
||||||
|
let salesforce_client = salesforce::connect(&args.sfdx_auth_url).await?;
|
||||||
|
let s3_client = aws_s3::connect(
|
||||||
|
args.s3_client_id(),
|
||||||
|
args.s3_client_secret(),
|
||||||
|
args.s3_endpoint(),
|
||||||
|
args.s3_region(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
salesforce_client.query(r#"SELECT Id, Title, FileType, FileExtension, Description,
|
||||||
|
ContentLocation, Origin, VersionData, ContentDocumentId, FirstPublishLocationId, PathOnClient
|
||||||
|
FROM ContentVersion
|
||||||
|
WHERE IsMajorVersion = TRUE
|
||||||
|
AND ContentLocation = 'S'
|
||||||
|
LIMIT 1\
|
||||||
|
"#)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if terminator::CONFIG.is_fired() {
|
||||||
|
info!("Received termination signal, exiting...");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
info!("Done.");*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use rustsf::{RestApi, AuthUrl};
|
||||||
|
|
||||||
|
pub(crate) mod models;
|
||||||
|
|
||||||
|
pub async fn connect(sfdx_auth_url: &str) -> Result<RestApi> {
|
||||||
|
|
||||||
|
let credentials = AuthUrl::new(sfdx_auth_url.to_string())
|
||||||
|
.context("Incorrect SFDX_AUTH_URL")?;
|
||||||
|
|
||||||
|
let client = rustsf::Client::new(credentials).await
|
||||||
|
.context("Unable to connect to Salesforce")?;
|
||||||
|
|
||||||
|
Ok(RestApi::new(client))
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use rustsf::DefSObject;
|
||||||
|
|
||||||
|
#[DefSObject(sobject_type = "ContentVersion", fields="owner,audit,system")]
|
||||||
|
pub(crate) struct ContentVersion {
|
||||||
|
|
||||||
|
/// The ID of the ContentDocument associated with this version.
|
||||||
|
#[serde(rename = "ContentDocumentId")]
|
||||||
|
content_document_id: Option<String>,
|
||||||
|
|
||||||
|
/// The location where the content is stored. Valid values include Salesforce, external, or linked content locations.
|
||||||
|
#[serde(rename = "ContentLocation")]
|
||||||
|
content_location: Option<String>,
|
||||||
|
|
||||||
|
/// The date and time when the file content was last modified.
|
||||||
|
#[serde(rename = "ContentModifiedDate")]
|
||||||
|
content_modified_date: Option<String>,
|
||||||
|
|
||||||
|
/// The ID of the user who last modified the file content.
|
||||||
|
#[serde(rename = "ContentModifiedById")]
|
||||||
|
content_modified_by_id: Option<String>,
|
||||||
|
|
||||||
|
/// The URL of the content when the file is stored outside Salesforce.
|
||||||
|
#[serde(rename = "ContentUrl")]
|
||||||
|
content_url: Option<String>,
|
||||||
|
|
||||||
|
/// The version number of the file.
|
||||||
|
#[serde(rename = "VersionNumber")]
|
||||||
|
version_number: Option<String>,
|
||||||
|
|
||||||
|
/// The title of the file.
|
||||||
|
#[serde(rename = "Title")]
|
||||||
|
title: Option<String>,
|
||||||
|
|
||||||
|
/// The description of the file.
|
||||||
|
#[serde(rename = "Description")]
|
||||||
|
description: Option<String>,
|
||||||
|
|
||||||
|
/// The reason this version of the file was created or changed.
|
||||||
|
#[serde(rename = "ReasonForChange")]
|
||||||
|
reason_for_change: Option<String>,
|
||||||
|
|
||||||
|
/// Controls whether users can share the file with other users, groups, or records.
|
||||||
|
#[serde(rename = "SharingOption")]
|
||||||
|
sharing_option: Option<String>,
|
||||||
|
|
||||||
|
/// Controls file privacy on records.
|
||||||
|
#[serde(rename = "SharingPrivacy")]
|
||||||
|
sharing_privacy: Option<String>,
|
||||||
|
|
||||||
|
/// The complete path of the file on the client machine when it was uploaded.
|
||||||
|
#[serde(rename = "PathOnClient")]
|
||||||
|
path_on_client: Option<String>,
|
||||||
|
|
||||||
|
/// Indicates whether this version is the latest version of the file.
|
||||||
|
#[serde(rename = "IsLatest")]
|
||||||
|
is_latest: bool,
|
||||||
|
|
||||||
|
/// Indicates whether this version is a major version.
|
||||||
|
#[serde(rename = "IsMajorVersion")]
|
||||||
|
is_major_version: bool,
|
||||||
|
|
||||||
|
/// Indicates whether the file can be used as an asset file.
|
||||||
|
#[serde(rename = "IsAssetEnabled")]
|
||||||
|
is_asset_enabled: bool,
|
||||||
|
|
||||||
|
/// Indicates whether the file is managed by an external client.
|
||||||
|
#[serde(rename = "IsClientManaged")]
|
||||||
|
is_client_managed: bool,
|
||||||
|
|
||||||
|
/// Indicates whether the file is available externally.
|
||||||
|
#[serde(rename = "IsExternallyAvailable")]
|
||||||
|
is_externally_available: bool,
|
||||||
|
|
||||||
|
/// The ID of the external data source for externally stored content.
|
||||||
|
#[serde(rename = "ExternalDataSourceId")]
|
||||||
|
external_data_source_id: Option<String>,
|
||||||
|
|
||||||
|
/// External document information used to identify externally stored content.
|
||||||
|
#[serde(rename = "ExternalDocumentInfo1")]
|
||||||
|
external_document_info1: Option<String>,
|
||||||
|
|
||||||
|
/// Additional external document information used to identify externally stored content.
|
||||||
|
#[serde(rename = "ExternalDocumentInfo2")]
|
||||||
|
external_document_info2: Option<String>,
|
||||||
|
|
||||||
|
/// The boost value used to promote featured content.
|
||||||
|
#[serde(rename = "FeaturedContentBoost")]
|
||||||
|
featured_content_boost: Option<i64>,
|
||||||
|
|
||||||
|
/// The date when the file was marked as featured content.
|
||||||
|
#[serde(rename = "FeaturedContentDate")]
|
||||||
|
featured_content_date: Option<String>,
|
||||||
|
|
||||||
|
/// The file extension of the uploaded file.
|
||||||
|
#[serde(rename = "FileExtension")]
|
||||||
|
file_extension: Option<String>,
|
||||||
|
|
||||||
|
/// The type of the file, such as PDF, PNG, WORD, or LINK.
|
||||||
|
#[serde(rename = "FileType")]
|
||||||
|
file_type: Option<String>,
|
||||||
|
|
||||||
|
/// The ID of the first location where the file was published.
|
||||||
|
#[serde(rename = "FirstPublishLocationId")]
|
||||||
|
first_publish_location_id: Option<String>,
|
||||||
|
|
||||||
|
/// The source from which the content originated.
|
||||||
|
#[serde(rename = "Origin")]
|
||||||
|
origin: Option<String>,
|
||||||
|
|
||||||
|
/// The ID of the Experience Cloud site or network associated with the file.
|
||||||
|
#[serde(rename = "NetworkId")]
|
||||||
|
network_id: Option<String>,
|
||||||
|
|
||||||
|
/// The number of positive ratings submitted for this file version.
|
||||||
|
#[serde(rename = "PositiveRatingCount")]
|
||||||
|
positive_rating_count: Option<i64>,
|
||||||
|
|
||||||
|
/// The publishing status of the file version.
|
||||||
|
#[serde(rename = "PublishStatus")]
|
||||||
|
publish_status: Option<String>,
|
||||||
|
|
||||||
|
/// A comma-separated list of tags associated with the file.
|
||||||
|
#[serde(rename = "TagCsv")]
|
||||||
|
tag_csv: Option<String>,
|
||||||
|
|
||||||
|
/// A text preview of the file content.
|
||||||
|
#[serde(rename = "TextPreview")]
|
||||||
|
text_preview: Option<String>,
|
||||||
|
|
||||||
|
/// The body of the file, represented as base64-encoded content when retrieved through the API.
|
||||||
|
#[serde(rename = "VersionData")]
|
||||||
|
version_data: Option<String>,
|
||||||
|
|
||||||
|
/// The size of the file in bytes.
|
||||||
|
#[serde(rename = "ContentSize")]
|
||||||
|
content_size: Option<i64>,
|
||||||
|
|
||||||
|
/// MD5 checksum for the file.
|
||||||
|
#[serde(rename = "Checksum")]
|
||||||
|
checksum: Option<String>,
|
||||||
|
|
||||||
|
/// The ID of the ContentBody record that stores the binary file data.
|
||||||
|
#[serde(rename = "ContentBodyId")]
|
||||||
|
content_body_id: Option<String>,
|
||||||
|
|
||||||
|
/// The ID of the user who created the file version.
|
||||||
|
#[serde(rename = "CreatedById")]
|
||||||
|
created_by_id: String,
|
||||||
|
|
||||||
|
/// The date and time when the file version was created.
|
||||||
|
#[serde(rename = "CreatedDate")]
|
||||||
|
created_date: String,
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use signal_hook::consts::TERM_SIGNALS;
|
||||||
|
use signal_hook::flag;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CONFIG: Terminator = Terminator::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Terminator {
|
||||||
|
term: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminator {
|
||||||
|
pub(crate) fn is_fired(&self) -> bool {
|
||||||
|
!self.term.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let term_now = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
// Register signals to set the flag to true
|
||||||
|
for sig in TERM_SIGNALS {
|
||||||
|
flag::register(*sig, Arc::clone(&term_now)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { term: term_now }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user