butterfly_dl/
lib.rs

1//! # Butterfly-dl Library
2//!
3//! A high-performance, memory-efficient library for downloading OpenStreetMap data files
4//! with intelligent source routing and minimal memory usage.
5//!
6//! ## Features
7//!
8//! - **Smart source routing**: S3 for planet files, HTTP for regional extracts
9//! - **Memory efficient**: <1GB RAM usage regardless of file size
10//! - **Streaming support**: Download directly to any AsyncWrite destination
11//! - **Progress tracking**: Optional progress callbacks for custom UIs
12//! - **Feature flags**: Optional S3 support to minimize dependencies
13//!
14//! ## Basic Usage
15//!
16//! ```rust,no_run
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!     // Download to file with auto-generated filename
20//!     butterfly_dl::get("europe/belgium", None).await?;
21//!     
22//!     // Download to specific file
23//!     butterfly_dl::get("planet", Some("./planet.pbf")).await?;
24//!     
25//!     // Stream download
26//!     let mut stream = butterfly_dl::get_stream("europe/monaco").await?;
27//!     // Use stream with any AsyncRead-compatible code
28//!     
29//!     Ok(())
30//! }
31//! ```
32//!
33//! ## Progress Tracking
34//!
35//! ```rust,no_run
36//! #[tokio::main]
37//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
38//!     butterfly_dl::get_with_progress(
39//!         "europe/belgium",
40//!         Some("belgium.pbf"),
41//!         |downloaded, total| {
42//!             println!("Progress: {}/{} bytes", downloaded, total);
43//!         }
44//!     ).await?;
45//!     
46//!     Ok(())
47//! }
48//! ```
49
50use std::sync::Arc;
51use tokio::io::AsyncRead;
52
53// Re-export core types that users might need
54pub use butterfly_common::{Error, Result};
55pub use crate::core::stream::{DownloadOptions, OverwriteBehavior};
56
57// Internal modules
58mod core;
59
60// C-compatible FFI bindings (optional)
61#[cfg(feature = "c-bindings")]
62pub mod ffi;
63
64/// Download a file to a destination
65///
66/// # Arguments
67/// * `source` - Source identifier (e.g., "planet", "europe", "europe/belgium")
68/// * `dest` - Optional destination file path. If None, auto-generates filename
69///
70/// # Examples
71/// ```rust,no_run
72/// # #[tokio::main]
73/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
74/// // Download with auto-generated filename
75/// butterfly_dl::get("europe/belgium", None).await?;
76///
77/// // Download to specific file
78/// butterfly_dl::get("planet", Some("./my-planet.pbf")).await?;
79/// # Ok(())
80/// # }
81/// ```
82pub async fn get(source: &str, dest: Option<&str>) -> Result<()> {
83    let downloader = core::Downloader::new();
84    let options = DownloadOptions::default();
85
86    let file_path = match dest {
87        Some(path) => path.to_string(),
88        None => core::resolve_output_filename(source),
89    };
90
91    downloader
92        .download_to_file(source, &file_path, &options)
93        .await
94}
95
96/// Download and return a stream
97///
98/// Returns an AsyncRead stream that can be used with any compatible code.
99/// The stream implements AsyncRead + Send + Unpin.
100///
101/// # Arguments
102/// * `source` - Source identifier (e.g., "planet", "europe", "europe/belgium")
103///
104/// # Returns
105/// * `impl AsyncRead + Send + Unpin` - Stream of the downloaded data
106///
107/// # Examples
108/// ```rust,no_run
109/// use tokio::io::AsyncReadExt;
110///
111/// # #[tokio::main]
112/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
113/// let mut stream = butterfly_dl::get_stream("europe/monaco").await?;
114/// let mut buffer = Vec::new();
115/// stream.read_to_end(&mut buffer).await?;
116/// println!("Downloaded {} bytes", buffer.len());
117/// # Ok(())
118/// # }
119/// ```
120pub async fn get_stream(source: &str) -> Result<impl AsyncRead + Send + Unpin> {
121    let downloader = core::Downloader::new();
122    let options = DownloadOptions::default();
123
124    let (stream, _total_size) = downloader.download_stream(source, &options).await?;
125    Ok(stream)
126}
127
128/// Download with progress tracking
129///
130/// Downloads a file with a progress callback that receives (downloaded_bytes, total_bytes).
131///
132/// # Arguments
133/// * `source` - Source identifier (e.g., "planet", "europe", "europe/belgium")
134/// * `dest` - Optional destination file path. If None, auto-generates filename
135/// * `progress` - Callback function that receives (downloaded, total) bytes
136///
137/// # Examples
138/// ```rust,no_run
139/// # #[tokio::main]
140/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
141/// butterfly_dl::get_with_progress(
142///     "europe/belgium",
143///     Some("belgium.pbf"),
144///     |downloaded, total| {
145///         let percent = (downloaded as f64 / total as f64) * 100.0;
146///         println!("Progress: {:.1}%", percent);
147///     }
148/// ).await?;
149/// # Ok(())
150/// # }
151/// ```
152pub async fn get_with_progress<F>(source: &str, dest: Option<&str>, progress: F) -> Result<()>
153where
154    F: Fn(u64, u64) + Send + Sync + 'static,
155{
156    let downloader = core::Downloader::new();
157    let options = DownloadOptions {
158        progress: Some(Arc::new(progress)),
159        ..Default::default()
160    };
161
162    let file_path = match dest {
163        Some(path) => path.to_string(),
164        None => core::resolve_output_filename(source),
165    };
166
167    downloader
168        .download_to_file(source, &file_path, &options)
169        .await
170}
171
172/// Download with custom options
173///
174/// Provides full control over download options including buffer size,
175/// connection limits, and progress tracking.
176///
177/// # Arguments
178/// * `source` - Source identifier
179/// * `dest` - Optional destination file path
180/// * `options` - Download options
181///
182/// # Examples
183/// ```rust,no_run
184/// use butterfly_dl::{DownloadOptions, OverwriteBehavior};
185/// use std::sync::Arc;
186///
187/// # #[tokio::main]
188/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
189/// let options = DownloadOptions {
190///     buffer_size: 128 * 1024, // 128KB buffer
191///     max_connections: 8,      // Limit to 8 connections
192///     overwrite: OverwriteBehavior::Force, // Overwrite without prompting
193///     progress: Some(Arc::new(|downloaded, total| {
194///         println!("Downloaded: {} / {}", downloaded, total);
195///     })),
196/// };
197///
198/// butterfly_dl::get_with_options("europe/belgium", None, options).await?;
199/// # Ok(())
200/// # }
201/// ```
202pub async fn get_with_options(
203    source: &str,
204    dest: Option<&str>,
205    options: DownloadOptions,
206) -> Result<()> {
207    let downloader = core::Downloader::new();
208
209    let file_path = match dest {
210        Some(path) => path.to_string(),
211        None => core::resolve_output_filename(source),
212    };
213
214    downloader
215        .download_to_file(source, &file_path, &options)
216        .await
217}
218
219/// Advanced API: Create a downloader with custom configuration
220///
221/// For advanced users who need to customize source URLs, S3 buckets, etc.
222///
223/// # Examples
224/// ```rust,no_run
225/// use butterfly_dl::{Downloader, SourceConfig};
226///
227/// # #[tokio::main]
228/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
229/// let config = SourceConfig {
230///     planet_http_url: "https://my-custom-mirror.org/planet.pbf".to_string(),
231///     ..Default::default()
232/// };
233///
234/// let downloader = Downloader::with_config(config);
235/// // Use downloader methods...
236/// # Ok(())
237/// # }
238/// ```
239pub use core::{Downloader, SourceConfig};
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use tempfile::tempdir;
245
246    #[tokio::test]
247    async fn test_get_with_tempfile() {
248        let dir = tempdir().unwrap();
249        let _file_path = dir.path().join("test.pbf");
250
251        // This would fail in real test without network, but validates the API
252        // get("europe/monaco", Some(file_path.to_str().unwrap())).await.unwrap();
253    }
254
255    #[test]
256    fn test_resolve_output_filename() {
257        assert_eq!(
258            core::resolve_output_filename("planet"),
259            "planet-latest.osm.pbf"
260        );
261        assert_eq!(
262            core::resolve_output_filename("europe"),
263            "europe-latest.osm.pbf"
264        );
265        assert_eq!(
266            core::resolve_output_filename("europe/belgium"),
267            "belgium-latest.osm.pbf"
268        );
269    }
270}