Skip to main content

typub_storage/
lib.rs

1//! S3-compatible storage client, status tracking, and asset types for typub.
2//!
3//! This crate provides:
4//! - `S3Storage` — S3-compatible storage client
5//! - `UploadResult` — result of an asset upload
6//! - `PendingAsset`, `PendingAssetList`, `DeferredAssets` — deferred asset types
7//! - `StatusTracker` — SQLite-backed publish status tracking
8//! - Asset upload orchestration (`materialize_external_assets`, etc.)
9//! - Pure utility functions for hash computation, URL construction, encoding, etc.
10//!
11//! Extracted per [[RFC-0007:C-SHARED-TYPES]] to enable adapter subcrates
12//! to handle asset uploads without depending on the main crate.
13
14// ============================================================================
15// Modules
16// ============================================================================
17
18mod deferred;
19mod encoding;
20mod s3;
21pub mod status;
22mod upload;
23mod url_mapping;
24
25// ============================================================================
26// Re-exports
27// ============================================================================
28
29// From typub-core
30pub use typub_core::AssetStrategy;
31
32// Deferred asset types
33pub use deferred::{DeferredAssets, PendingAsset, PendingAssetList, build_pending_asset_list};
34
35// Encoding utilities
36pub use encoding::{base64_encode, to_data_uri};
37
38// S3 storage
39pub use s3::{S3Storage, UploadResult};
40
41// URL mapping utilities
42pub use url_mapping::{
43    build_image_marker_url_map, build_image_src_url_map, build_preview_file_url_map,
44    key_candidates, resolve_image_reference_url,
45};
46
47// Upload orchestration
48pub use upload::{
49    AssetAnalysis, AssetInfo, analyze_assets, build_resolved_url_map, materialize_external_assets,
50    materialize_external_assets_with_status, materialize_with_analysis, upload_pending_assets,
51};
52
53// Status tracking (re-export commonly used types)
54pub use status::{
55    AssetUploadRecord, LifecycleAction, PlatformStatus, PostStatus, PublishResult, StatusDatabase,
56    StatusTracker, determine_lifecycle_action, validate_remote_status,
57};
58
59// ============================================================================
60// Utility Functions
61// ============================================================================
62
63use std::path::Path;
64
65/// Replace placeholder tokens in content with remote URLs.
66///
67/// Per [[RFC-0004:C-PIPELINE-INTEGRATION]], placeholder tokens are in the format
68/// `{{ASSET:<index>}}`. This function replaces all such tokens with the corresponding
69/// remote URLs from the provided map.
70///
71/// # Arguments
72///
73/// * `content` - The content string containing placeholder tokens
74/// * `url_map` - A map from asset index to remote URL
75///
76/// # Returns
77///
78/// The content with all placeholder tokens replaced.
79pub fn replace_asset_placeholders(
80    content: &str,
81    url_map: &std::collections::HashMap<usize, String>,
82) -> String {
83    let mut result = content.to_string();
84
85    for (index, url) in url_map {
86        let placeholder = format!("{{{{ASSET:{}}}}}", index);
87        result = result.replace(&placeholder, url);
88    }
89
90    result
91}
92
93/// Determine MIME type from file extension using mime_guess.
94/// Returns "application/octet-stream" for unknown types.
95pub fn mime_type_from_path(path: &Path) -> &'static str {
96    mime_guess::from_path(path)
97        .first_raw()
98        .unwrap_or("application/octet-stream")
99}
100
101// ============================================================================
102// Tests
103// ============================================================================
104
105#[cfg(test)]
106mod tests {
107    #![allow(clippy::expect_used)]
108
109    use super::*;
110
111    #[test]
112    fn test_replace_asset_placeholders_single() {
113        let content = "Here is an image: {{ASSET:0}}";
114        let mut url_map = std::collections::HashMap::new();
115        url_map.insert(0, "https://cdn.example.com/abc123.png".to_string());
116
117        let result = replace_asset_placeholders(content, &url_map);
118        assert_eq!(
119            result,
120            "Here is an image: https://cdn.example.com/abc123.png"
121        );
122    }
123
124    #[test]
125    fn test_replace_asset_placeholders_multiple() {
126        let content = "First: {{ASSET:0}}, Second: {{ASSET:1}}, First again: {{ASSET:0}}";
127        let mut url_map = std::collections::HashMap::new();
128        url_map.insert(0, "https://cdn.example.com/first.png".to_string());
129        url_map.insert(1, "https://cdn.example.com/second.jpg".to_string());
130
131        let result = replace_asset_placeholders(content, &url_map);
132        assert_eq!(
133            result,
134            "First: https://cdn.example.com/first.png, Second: https://cdn.example.com/second.jpg, First again: https://cdn.example.com/first.png"
135        );
136    }
137
138    #[test]
139    fn test_replace_asset_placeholders_missing() {
140        let content = "Here is an image: {{ASSET:99}}";
141        let url_map = std::collections::HashMap::new();
142
143        let result = replace_asset_placeholders(content, &url_map);
144        assert_eq!(result, "Here is an image: {{ASSET:99}}");
145    }
146
147    #[test]
148    fn test_mime_type_from_path() {
149        assert_eq!(mime_type_from_path(Path::new("image.png")), "image/png");
150        assert_eq!(mime_type_from_path(Path::new("photo.JPEG")), "image/jpeg");
151        assert_eq!(mime_type_from_path(Path::new("doc.pdf")), "application/pdf");
152        assert_eq!(
153            mime_type_from_path(Path::new("unknown.unknownext123")),
154            "application/octet-stream"
155        );
156    }
157}