oxen-server 0.53.0

Oxen server is a fast data version control backend, supporting local disk and S3. Self host your repositories on your own storage, or use the hosted platform on Oxen.ai. Stores, syncs, and serves versioned datasets, model checkpoints, game assets, studio media, and any large data. Use the oxen CLI to push and pull from the oxen server.
use actix_web::{HttpRequest, HttpResponse};

use crate::controllers;
use crate::errors::OxenHttpError;
use crate::helpers::get_repo;
use crate::params::{app_data, parse_resource, path_param};

use liboxen::model::merkle_tree::node::EMerkleTreeNode;
use liboxen::view::FileWithHash;
use liboxen::{constants, repositories};

/// Export resource as a zip
#[utoipa::path(
    get,
    path = "/api/repos/{namespace}/{repo_name}/export/download/{resource}",
    tag = "Export",
    description = "Download a directory as a zip archive. Subject to size limits.",
    params(
        ("namespace" = String, Path, description = "Namespace of the repository", example = "ox"),
        ("repo_name" = String, Path, description = "Name of the repository", example = "Wiki-Text"),
        ("resource" = String, Path, description = "Path to directory and branch/commit (e.g. main/data)", example = "main/data"),
    ),
    responses(
        (status = 200, description = "Zip archive of the directory", content_type = "application/zip"),
        (status = 400, description = "Download size exceeds limit"),
        (status = 404, description = "Repository or resource not found")
    )
)]
pub async fn download_zip(req: HttpRequest) -> Result<HttpResponse, OxenHttpError> {
    let app_data = app_data(&req)?;
    let namespace = path_param(&req, "namespace")?.to_string();
    let repo_name = path_param(&req, "repo_name")?.to_string();
    let repo = get_repo(app_data, &namespace, &repo_name)?;

    let resource = parse_resource(&req, &repo)?;
    let directory = resource.path.clone();
    let commit = resource.commit.ok_or(OxenHttpError::NotFound)?;

    let Some(dir_node) =
        repositories::tree::get_dir_with_children_recursive(&repo, &commit, &directory, None)?
    else {
        return Err(OxenHttpError::NotFound);
    };

    log::debug!("download_dir_as_zip found dir node: {dir_node:?}");
    let files = dir_node.list_files()?;

    let total_bytes: u64 = files
        .values()
        .filter_map(|node| {
            if let EMerkleTreeNode::File(file_node) = &node.node {
                Some(file_node.num_bytes())
            } else {
                None
            }
        })
        .sum();
    if total_bytes > constants::MAX_ZIP_DOWNLOAD_SIZE {
        return Err(OxenHttpError::BadRequest(format!("Download request exceeded size limit ({} bytes) for zip file downloads: found {} bytes", constants::MAX_ZIP_DOWNLOAD_SIZE, total_bytes).into()));
    }

    let files_with_hash: Vec<FileWithHash> = files
        .into_iter()
        .map(|f| FileWithHash {
            path: f.0,
            hash: f.1.hash.to_string(),
        })
        .collect();

    log::debug!(
        "download_dir_as_zip found {} files with total size {}",
        files_with_hash.len(),
        total_bytes
    );

    let response = controllers::versions::stream_versions_zip(&repo, files_with_hash).await?;

    Ok(response)
}