i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
//! Upload a rendered session to S3-compatible storage and return a presigned URL.
//!
//! Reuses the sync module's S3 client builder so the user's existing cloud
//! config (`[cloud]` in config.toml + `ISELF_SYNC_*` / `AWS_*` env) is the
//! single source of truth for credentials and endpoint. No new auth surface.

use crate::sync::{build_s3_client, SyncConfig};
use anyhow::{anyhow, Context, Result};
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::primitives::ByteStream;
use std::time::Duration;

pub struct UploadResult {
    pub bucket: String,
    pub key: String,
    /// Presigned GET URL anyone can open for `expires_in` seconds. Treat as
    /// a credential — anyone with the URL can read the object until it expires.
    pub presigned_url: String,
    pub expires_in: Duration,
}

/// Upload `body` to `s3://<bucket>/<key>` and return a presigned GET URL.
pub async fn upload_with_presigned_url(
    cfg: &SyncConfig,
    key: &str,
    body: Vec<u8>,
    content_type: &str,
    expires_in: Duration,
) -> Result<UploadResult> {
    if cfg.bucket.is_empty() {
        return Err(anyhow!(
            "no bucket configured. Set [cloud] bucket in ~/.i-self/config.toml \
             or ISELF_SYNC_BUCKET env var."
        ));
    }
    if expires_in.as_secs() == 0 || expires_in.as_secs() > 7 * 24 * 60 * 60 {
        // S3 SigV4 caps at 7 days. Reject 0 too — a URL that's already expired
        // is never what the user wanted.
        return Err(anyhow!(
            "expires_in must be between 1 second and 7 days (got {} seconds)",
            expires_in.as_secs()
        ));
    }

    let client = build_s3_client(cfg).await?;

    client
        .put_object()
        .bucket(&cfg.bucket)
        .key(key)
        .content_type(content_type)
        .body(ByteStream::from(body))
        .send()
        .await
        .with_context(|| format!("PutObject {}/{}", cfg.bucket, key))?;

    let presigning_config = PresigningConfig::builder()
        .expires_in(expires_in)
        .build()
        .context("PresigningConfig::build")?;

    let presigned = client
        .get_object()
        .bucket(&cfg.bucket)
        .key(key)
        .presigned(presigning_config)
        .await
        .with_context(|| format!("presign GetObject {}/{}", cfg.bucket, key))?;

    Ok(UploadResult {
        bucket: cfg.bucket.clone(),
        key: key.to_string(),
        presigned_url: presigned.uri().to_string(),
        expires_in,
    })
}