a3s 0.10.1

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::sync::Arc;

use a3s_boot::{
    controller, BootError, BootRequest, BootResponse, Result as BootResult, StreamableFile,
};
use futures::stream;
use tokio::io::AsyncReadExt;

use super::model::{
    CopyArtifactRequest, RestoreVersionRequest, RevisionRequest, SaveArtifactRequest,
    SaveFolderRequest, WorkArtifact, WorkArtifactVersion, WorkFolder,
};
use super::service::WorkService;
use super::storage;

pub(super) struct WorkController {
    service: Arc<WorkService>,
}

impl WorkController {
    pub(super) fn new(service: Arc<WorkService>) -> Self {
        Self { service }
    }
}

#[controller("/v1/work")]
impl WorkController {
    #[get("/library")]
    async fn library(
        &self,
        #[query("includeTrash")] include_trash: Option<bool>,
    ) -> BootResult<serde_json::Value> {
        self.service.library(include_trash.unwrap_or(false)).await
    }

    #[get("/artifacts/{id}")]
    async fn artifact(&self, #[param("id")] id: String) -> BootResult<WorkArtifact> {
        self.service.artifact(&id).await
    }

    #[put("/artifacts/{id}")]
    async fn save_artifact(
        &self,
        #[param("id")] id: String,
        #[body] request: SaveArtifactRequest,
    ) -> BootResult<WorkArtifact> {
        self.service.save_artifact(&id, request).await
    }

    #[delete("/artifacts/{id}")]
    async fn trash_artifact(
        &self,
        #[param("id")] id: String,
        #[body] request: RevisionRequest,
    ) -> BootResult<WorkArtifact> {
        self.service.trash_artifact(&id, request).await
    }

    #[post("/artifacts/{id}/restore")]
    async fn restore_artifact(
        &self,
        #[param("id")] id: String,
        #[body] request: RevisionRequest,
    ) -> BootResult<WorkArtifact> {
        self.service.restore_artifact(&id, request).await
    }

    #[delete("/artifacts/{id}/purge")]
    async fn purge_artifact(&self, #[param("id")] id: String) -> BootResult<serde_json::Value> {
        self.service.purge_artifact(&id).await
    }

    #[post("/artifacts/{id}/copy")]
    async fn copy_artifact(
        &self,
        #[param("id")] id: String,
        #[body] request: CopyArtifactRequest,
    ) -> BootResult<WorkArtifact> {
        self.service.copy_artifact(&id, request).await
    }

    #[get("/artifacts/{id}/versions")]
    async fn versions(&self, #[param("id")] id: String) -> BootResult<Vec<WorkArtifactVersion>> {
        self.service.versions(&id).await
    }

    #[post("/artifacts/{id}/versions/restore")]
    async fn restore_version(
        &self,
        #[param("id")] id: String,
        #[body] request: RestoreVersionRequest,
    ) -> BootResult<WorkArtifact> {
        self.service.restore_version(&id, request).await
    }

    #[put("/artifacts/{id}/source")]
    async fn upload_source(
        &self,
        #[param("id")] id: String,
        #[query("expectedRevision")] expected_revision: u64,
        #[query("fileName")] file_name: String,
        #[request] request: BootRequest,
    ) -> BootResult<WorkArtifact> {
        self.service
            .upload_source(
                &id,
                expected_revision,
                file_name,
                request.content_type().map(str::to_string),
                request.body(),
            )
            .await
    }

    #[get("/artifacts/{id}/source", raw)]
    async fn download_source(&self, #[param("id")] id: String) -> BootResult<BootResponse> {
        let descriptor = self.service.source(&id).await?;
        let file = tokio::fs::File::open(&descriptor.path)
            .await
            .map_err(|error| storage::storage_error(&descriptor.path, error))?;
        let stream = stream::try_unfold(file, |mut file| async move {
            let mut bytes = vec![0; 64 * 1024];
            let count = file.read(&mut bytes).await.map_err(BootError::Io)?;
            if count == 0 {
                return Ok(None);
            }
            bytes.truncate(count);
            Ok(Some((bytes, file)))
        });
        let file = StreamableFile::stream(stream)
            .with_content_type(descriptor.metadata.content_type)
            .with_content_length(descriptor.metadata.size)
            .with_attachment(descriptor.metadata.name)?;
        Ok(BootResponse::streamable_file(file).with_header("x-content-type-options", "nosniff"))
    }

    #[put("/folders/{id}")]
    async fn save_folder(
        &self,
        #[param("id")] id: String,
        #[body] request: SaveFolderRequest,
    ) -> BootResult<WorkFolder> {
        self.service.save_folder(&id, request).await
    }

    #[delete("/folders/{id}")]
    async fn trash_folder(
        &self,
        #[param("id")] id: String,
        #[body] request: RevisionRequest,
    ) -> BootResult<WorkFolder> {
        self.service.trash_folder(&id, request).await
    }

    #[post("/folders/{id}/restore")]
    async fn restore_folder(
        &self,
        #[param("id")] id: String,
        #[body] request: RevisionRequest,
    ) -> BootResult<WorkFolder> {
        self.service.restore_folder(&id, request).await
    }

    #[delete("/folders/{id}/purge")]
    async fn purge_folder(&self, #[param("id")] id: String) -> BootResult<serde_json::Value> {
        self.service.purge_folder(&id).await
    }
}