-
Notifications
You must be signed in to change notification settings - Fork 15
feat: rclone-privider #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ff360a
5931221
9409f0f
b222b13
ea356ed
ba83b6b
576c055
31e55a7
9bb8303
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,9 @@ | ||
| # Git | ||
| .git | ||
| .gitignore | ||
|
|
||
| # MD files | ||
| CHANGELOG.md | ||
| README.md | ||
| RELEASE.md | ||
|
|
||
| #IDE configurations | ||
| .idea | ||
| target | ||
| dump.rdb | ||
| .superpowers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ | |
| .claude | ||
|
|
||
| /docs | ||
| .superpowers | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,15 @@ RUN ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ | |
| | tar -xjf - -C /usr/local/bin sqlcmd \ | ||
| && chmod +x /usr/local/bin/sqlcmd | ||
|
|
||
| # ========================= | ||
| # rclone (all storage backends) | ||
| # ========================= | ||
| ARG RCLONE_VERSION=1.75.1 | ||
| RUN ARCH=$(dpkg --print-architecture) \ | ||
| && curl -fsSL -o /tmp/rclone.deb "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-${ARCH}.deb" \ | ||
| && dpkg -i /tmp/rclone.deb \ | ||
|
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: sed -n '1,70p;135,160p' docker/Dockerfile
printf '\nRelevant rclone references:\n'
rg -n 'RCLONE_VERSION|rclone\.deb|downloads\.rclone\.org|dpkg -i' docker/Dockerfile docker-compose.prod.ymlRepository: Portabase/agent Length of output: 3760 🏁 Script executed: python3 - <<'PY'
from pathlib import Path
p = Path("docker/Dockerfile")
lines = p.read_text().splitlines()
for start, end in ((1, 70), (140, 155)):
print(f"--- lines {start}-{end} ---")
for n in range(start, min(end, len(lines)) + 1):
print(f"{n}: {lines[n-1]}")
PYRepository: Portabase/agent Length of output: 3364 Security Misconfiguration Reachability: Internal Verify the rclone package before installation. These commands download an executable 🤖 Prompt for AI Agents |
||
| && rm /tmp/rclone.deb | ||
|
|
||
| ARG TARGETARCH | ||
|
|
||
| # ========================= | ||
|
|
@@ -134,6 +143,12 @@ RUN apt-get update && apt-get install -y \ | |
| firebird3.0-utils \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| ARG RCLONE_VERSION=1.75.1 | ||
| RUN ARCH=$(dpkg --print-architecture) \ | ||
| && curl -fsSL -o /tmp/rclone.deb "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-${ARCH}.deb" \ | ||
| && dpkg -i /tmp/rclone.deb \ | ||
| && rm /tmp/rclone.deb | ||
|
|
||
| ENV DOTNET_ROOT=/usr/local/dotnet | ||
| RUN curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \ | ||
| && chmod +x /tmp/dotnet-install.sh \ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| use anyhow::{Context, Result, bail}; | ||
| use bytes::Bytes; | ||
| use futures::{Stream, StreamExt}; | ||
| use std::io::Write; | ||
| use std::path::Path; | ||
| use std::pin::Pin; | ||
| use std::process::Stdio; | ||
| use tempfile::NamedTempFile; | ||
| use tokio::io::{AsyncReadExt, AsyncWriteExt}; | ||
| use tokio::process::Command; | ||
| use tracing::info; | ||
|
|
||
| const BLOCKED_BACKEND_TYPES: [&str; 13] = [ | ||
| "local", | ||
| "alias", | ||
| "crypt", | ||
| "chunker", | ||
| "compress", | ||
| "union", | ||
| "combine", | ||
| "hasher", | ||
| "archive", | ||
| "cache", | ||
| "memory", | ||
| "http", | ||
| "googlephotos", | ||
| ]; | ||
|
|
||
| fn sections(config_text: &str) -> Vec<(String, Option<String>)> { | ||
| let mut out: Vec<(String, Option<String>)> = Vec::new(); | ||
|
|
||
| for line in config_text.lines() { | ||
| let line = line.trim(); | ||
|
|
||
| if line.starts_with('[') && line.ends_with(']') && line.len() > 2 { | ||
| out.push((line[1..line.len() - 1].trim().to_string(), None)); | ||
| continue; | ||
| } | ||
|
|
||
| let Some((key, value)) = line.split_once('=') else { | ||
| continue; | ||
| }; | ||
|
|
||
| if key.trim().eq_ignore_ascii_case("type") | ||
| && let Some(current) = out.last_mut() | ||
| && current.1.is_none() | ||
| { | ||
| current.1 = Some(value.trim().to_ascii_lowercase()); | ||
| } | ||
| } | ||
|
|
||
| out | ||
| } | ||
|
|
||
|
|
||
| pub fn validate_config(config_text: &str, remote_name: &str) -> Result<()> { | ||
| let sections = sections(config_text); | ||
|
|
||
| if sections.is_empty() { | ||
| bail!("rclone config contains no remote sections"); | ||
| } | ||
|
|
||
| for (name, backend) in §ions { | ||
| let Some(backend) = backend else { continue }; | ||
| if BLOCKED_BACKEND_TYPES.contains(&backend.as_str()) { | ||
| bail!("rclone backend type '{backend}' is not allowed (remote '{name}')"); | ||
| } | ||
| } | ||
|
|
||
| if !sections.iter().any(|(name, _)| name == remote_name) { | ||
| let available: Vec<&str> = sections.iter().map(|(name, _)| name.as_str()).collect(); | ||
| bail!( | ||
| "remote '{remote_name}' is not defined in the rclone config (available: {})", | ||
| available.join(", ") | ||
| ); | ||
| } | ||
|
RambokDev marked this conversation as resolved.
|
||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// `<remote>:<remote_path>/<remote_file_path>` | ||
| pub fn remote_target(remote_name: &str, remote_path: &str, remote_file_path: &str) -> String { | ||
| let base = remote_path.trim().trim_matches('/'); | ||
|
|
||
| if base.is_empty() { | ||
| format!("{remote_name}:{remote_file_path}") | ||
| } else { | ||
| format!("{remote_name}:{base}/{remote_file_path}") | ||
| } | ||
| } | ||
|
|
||
| pub type RcloneStream = Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>; | ||
|
|
||
| pub fn write_config(config_text: &str) -> Result<NamedTempFile> { | ||
| let mut file = NamedTempFile::new().context("failed to create rclone config temp file")?; | ||
|
|
||
| #[cfg(unix)] | ||
| { | ||
| use std::os::unix::fs::PermissionsExt; | ||
| std::fs::set_permissions(file.path(), std::fs::Permissions::from_mode(0o600)) | ||
| .context("failed to restrict rclone config permissions")?; | ||
| } | ||
|
|
||
| file.write_all(config_text.as_bytes()) | ||
| .context("failed to write rclone config")?; | ||
| file.flush().context("failed to flush rclone config")?; | ||
|
|
||
| Ok(file) | ||
| } | ||
|
|
||
| pub async fn rcat(config_path: &Path, target: &str, mut stream: RcloneStream) -> Result<()> { | ||
| info!("rclone rcat -> {}", target); | ||
|
|
||
| let mut child = Command::new("rclone") | ||
| .arg("--config") | ||
| .arg(config_path) | ||
| .arg("--contimeout") | ||
| .arg("30s") | ||
| .arg("--timeout") | ||
| .arg("5m") | ||
| .arg("--retries") | ||
| .arg("1") | ||
| .arg("--low-level-retries") | ||
| .arg("3") | ||
| .arg("rcat") | ||
| .arg(target) | ||
| .stdin(Stdio::piped()) | ||
| .stdout(Stdio::null()) | ||
| .stderr(Stdio::piped()) | ||
| .spawn() | ||
| .context("failed to spawn rclone (is the binary installed in this image?)")?; | ||
|
|
||
| let mut stderr_pipe = child.stderr.take().context("rclone stderr unavailable")?; | ||
| let stderr_task = tokio::spawn(async move { | ||
| let mut buf = String::new(); | ||
| let _ = stderr_pipe.read_to_string(&mut buf).await; | ||
| buf | ||
| }); | ||
|
|
||
| let mut stdin = child.stdin.take().context("rclone stdin unavailable")?; | ||
|
|
||
| while let Some(chunk) = stream.next().await { | ||
| let chunk = match chunk { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| let _ = child.start_kill(); | ||
| let _ = child.wait().await; | ||
| return Err(e).context("backup stream failed"); | ||
| } | ||
| }; | ||
|
|
||
| if stdin.write_all(&chunk).await.is_err() { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| let _ = stdin.flush().await; | ||
| drop(stdin); | ||
|
|
||
| let status = child.wait().await.context("failed to wait for rclone")?; | ||
| let stderr = stderr_task.await.unwrap_or_default(); | ||
|
|
||
| if !status.success() { | ||
| bail!("rclone rcat failed ({status}): {}", stderr.trim()); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.