1use anyhow::{Result, anyhow};
4use oxigeo_core::io::{DataSource, FileDataSource};
5use std::sync::OnceLock;
6
7static TOKIO_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
8
9fn get_runtime() -> Result<&'static tokio::runtime::Runtime> {
13 if let Some(rt) = TOKIO_RUNTIME.get() {
14 return Ok(rt);
15 }
16 let rt = tokio::runtime::Runtime::new()
17 .map_err(|e| anyhow!("failed to create tokio runtime for cloud I/O: {}", e))?;
18 let _ = TOKIO_RUNTIME.set(rt);
21 TOKIO_RUNTIME
22 .get()
23 .ok_or_else(|| anyhow!("tokio runtime unavailable after init"))
24}
25
26pub fn is_cloud_uri(uri: &str) -> bool {
28 uri.starts_with("s3://") || uri.starts_with("gs://") || uri.starts_with("az://")
29}
30
31pub fn error_for_cloud_write(uri: &str) -> anyhow::Error {
33 anyhow!("cloud write not yet supported: {uri}; please write locally then upload")
34}
35
36pub fn open_datasource(uri: &str) -> Result<Box<dyn DataSource>> {
45 if let Some(path) = uri.strip_prefix("file://") {
46 return Ok(Box::new(
47 FileDataSource::open(path).map_err(|e| anyhow!("{}", e))?,
48 ));
49 }
50
51 if is_cloud_uri(uri) {
52 let rt = get_runtime()?;
53 let ds = rt.block_on(open_cloud_datasource(uri))?;
54 return Ok(ds);
55 }
56
57 Ok(Box::new(
59 FileDataSource::open(uri).map_err(|e| anyhow!("{}", e))?,
60 ))
61}
62
63async fn open_cloud_datasource(uri: &str) -> Result<Box<dyn DataSource>> {
64 let (backend, bucket, key) = oxigeo_rs3gw::parse_url(uri).map_err(|e| anyhow!("{}", e))?;
65 let storage = backend
66 .create_storage()
67 .await
68 .map_err(|e| anyhow!("{}", e))?;
69 let ds = oxigeo_rs3gw::Rs3gwDataSource::new(storage, bucket, key)
70 .await
71 .map_err(|e| anyhow!("{}", e))?;
72 Ok(Box::new(ds))
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn test_is_cloud_uri() {
81 assert!(is_cloud_uri("s3://bucket/key"));
82 assert!(is_cloud_uri("gs://bucket/obj"));
83 assert!(is_cloud_uri("az://container/blob"));
84 assert!(!is_cloud_uri("/local/path.tif"));
85 assert!(!is_cloud_uri("file:///local.tif"));
86 assert!(!is_cloud_uri("relative/path.tif"));
87 }
88
89 #[test]
90 fn test_error_for_cloud_write() {
91 let err = error_for_cloud_write("s3://my-bucket/output.tif");
92 let msg = err.to_string();
93 assert!(msg.contains("s3://my-bucket/output.tif"));
94 assert!(msg.contains("not yet supported"));
95 }
96
97 #[test]
98 fn test_open_datasource_file_path() {
99 let dir = std::env::temp_dir();
100 let path = dir.join("cloud_test_direct.bin");
101 std::fs::write(&path, b"test data").expect("write temp file");
102 let result = open_datasource(path.to_str().expect("valid path"));
103 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
104 }
105
106 #[test]
107 fn test_open_datasource_file_uri() {
108 let dir = std::env::temp_dir();
109 let path = dir.join("cloud_test_uri.bin");
110 std::fs::write(&path, b"test data").expect("write temp file");
111 let uri = format!("file://{}", path.display());
112 let result = open_datasource(&uri);
113 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
114 }
115
116 #[test]
117 fn test_cloud_uri_classification_comprehensive() {
118 assert!(is_cloud_uri("s3://bucket/path/to/file.tif"));
120 assert!(is_cloud_uri("gs://my-gcs-bucket/dir/file.tif"));
121 assert!(is_cloud_uri("az://mycontainer/blob/path.tif"));
122
123 assert!(!is_cloud_uri("file:///data/local.tif"));
125 assert!(!is_cloud_uri("/absolute/path.tif"));
126 assert!(!is_cloud_uri("relative/path.tif"));
127 assert!(!is_cloud_uri("http://example.com/file.tif"));
128 assert!(!is_cloud_uri("https://example.com/file.tif"));
129 assert!(!is_cloud_uri(""));
130 }
131}