photography

A tool for managing large amounts of images semi-automatically
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/photography.git
Log | Files | Refs

mod.rs (10365B)


      1 use std::path::PathBuf;
      2 use std::sync::Arc;
      3 
      4 use async_nats::jetstream;
      5 use async_nats::jetstream::AckKind;
      6 use async_nats::jetstream::consumer::Consumer;
      7 use async_nats::jetstream::consumer::pull::Config;
      8 use c2pa::Reader;
      9 use futures::StreamExt;
     10 use simple_c2pa::{
     11     ApplicationInfo, Certificate, ContentCredentials, create_content_credentials_certificate,
     12     create_root_certificate, ExifData, FileData,
     13 };
     14 use tracing::{debug, error, info};
     15 
     16 const APP_NAME: &str = "MTRNord_Photography_Manager";
     17 const USERNAME: &str = "MTRNord";
     18 
     19 pub fn generate_certificates(
     20     c2pa_folder: PathBuf,
     21 ) -> color_eyre::Result<(Arc<Certificate>, Arc<Certificate>)> {
     22     let c2pa_folder = c2pa_folder.join("certificates");
     23     // Create the folder if it doesn't exist
     24     std::fs::create_dir_all(&c2pa_folder)?;
     25 
     26     // Generate the certificates
     27     let root_certificate = create_root_certificate(Some(USERNAME), None)?;
     28     // Write the root certificate to a file
     29     let root_certificate_data = root_certificate.get_certificate_bytes()?;
     30     let root_private_key_data = root_certificate.get_private_key_bytes()?;
     31     let root_certificate_path = c2pa_folder.join("root_certificate.crt");
     32     let root_private_key_path = c2pa_folder.join("root_private_key.pem");
     33     std::fs::write(&root_certificate_path, root_certificate_data)?;
     34     std::fs::write(&root_private_key_path, root_private_key_data)?;
     35 
     36     // Create the content credentials certificate
     37 
     38     let content_credentials_certificate = create_content_credentials_certificate(
     39         Some(root_certificate.clone()),
     40         Some(USERNAME),
     41         None,
     42     )?;
     43 
     44     // Write the content credentials certificate to a file
     45     let content_credentials_certificate_data =
     46         content_credentials_certificate.get_certificate_bytes()?;
     47     let content_credentials_private_key_data =
     48         content_credentials_certificate.get_private_key_bytes()?;
     49     let content_credentials_certificate_path =
     50         c2pa_folder.join("content_credentials_certificate.crt");
     51     let content_credentials_private_key_path =
     52         c2pa_folder.join("content_credentials_private_key.pem");
     53     std::fs::write(
     54         &content_credentials_certificate_path,
     55         content_credentials_certificate_data,
     56     )?;
     57     std::fs::write(
     58         &content_credentials_private_key_path,
     59         content_credentials_private_key_data,
     60     )?;
     61 
     62     Ok((root_certificate, content_credentials_certificate))
     63 }
     64 
     65 fn load_certificates(
     66     c2pa_folder: PathBuf,
     67 ) -> color_eyre::Result<(Arc<Certificate>, Arc<Certificate>)> {
     68     let c2pa_folder = c2pa_folder.join("certificates");
     69     let root_certificate_path = c2pa_folder.join("root_certificate.crt");
     70     let root_private_key_path = c2pa_folder.join("root_private_key.pem");
     71     let content_credentials_certificate_path =
     72         c2pa_folder.join("content_credentials_certificate.crt");
     73     let content_credentials_private_key_path =
     74         c2pa_folder.join("content_credentials_private_key.pem");
     75 
     76     let root_certificate_data = std::fs::read(&root_certificate_path)?;
     77     let root_private_key_data = std::fs::read(&root_private_key_path)?;
     78     let content_credentials_certificate_data =
     79         std::fs::read(&content_credentials_certificate_path)?;
     80     let content_credentials_private_key_data =
     81         std::fs::read(&content_credentials_private_key_path)?;
     82 
     83     let root_certificate_filedata = FileData::new(None, Some(root_certificate_data), None);
     84     let root_private_key_filedata = FileData::new(None, Some(root_private_key_data), None);
     85     let content_credentials_certificate_filedata =
     86         FileData::new(None, Some(content_credentials_certificate_data), None);
     87     let content_credentials_private_key_filedata =
     88         FileData::new(None, Some(content_credentials_private_key_data), None);
     89 
     90     let root_certificate =
     91         Certificate::new(root_certificate_filedata, root_private_key_filedata, None);
     92     let content_credentials_certificate = Certificate::new(
     93         content_credentials_certificate_filedata,
     94         content_credentials_private_key_filedata,
     95         Some(root_certificate.clone()),
     96     );
     97 
     98     Ok((root_certificate, content_credentials_certificate))
     99 }
    100 
    101 pub fn sign_image(
    102     export_path: PathBuf,
    103     image_data: Arc<FileData>,
    104     exif_data: Option<ExifData>,
    105     content_credentials_certificate: Arc<Certificate>,
    106 ) -> color_eyre::Result<()> {
    107     let app_info = ApplicationInfo::new(APP_NAME.to_string(), "0.1.0".to_string(), None);
    108     let cc = ContentCredentials::new(content_credentials_certificate, image_data, Some(app_info));
    109 
    110     let gpg_fingerprint = "9768 CA63 F48D 3609 8567 A59D AEDE 3887 B155 1783";
    111     let gpg_key = "AEDE3887B1551783";
    112 
    113     cc.add_created_assertion()?;
    114     cc.add_restricted_ai_training_assertions()?;
    115     cc.add_instagram_assertion(USERNAME, USERNAME)?;
    116     cc.add_website_assertion("https://mtrnord.blog".to_string())?;
    117     cc.add_pgp_assertion(gpg_fingerprint, gpg_key)?;
    118 
    119     if let Some(exif_data) = exif_data {
    120         cc.add_exif_assertion(exif_data)?;
    121     }
    122 
    123     cc.embed_manifest(Some(export_path.clone()))?;
    124 
    125     // Bugfix: Delete any residual file with the same name and extension in /tmp
    126     let file_name = export_path.file_name().unwrap().to_str().unwrap();
    127     let tmp_file = std::env::temp_dir().join(file_name);
    128     if tmp_file.exists() {
    129         std::fs::remove_file(tmp_file)?;
    130     }
    131 
    132     Ok(())
    133 }
    134 
    135 #[derive(Debug, Clone)]
    136 pub(crate) struct C2PASigner {
    137     // Export folder to move files to
    138     export_folder: PathBuf,
    139     // C2PA folder to store certificates
    140     c2pa_folder: PathBuf,
    141     // Nats consumer to work with
    142     consumer: Consumer<Config>,
    143 }
    144 
    145 impl C2PASigner {
    146     pub async fn new(
    147         export_folder: PathBuf,
    148         c2pa_folder: PathBuf,
    149         import_stream: jetstream::stream::Stream,
    150     ) -> color_eyre::Result<Self> {
    151         let consumer = import_stream
    152             .create_consumer(Config {
    153                 durable_name: Some("processor-2".to_string()),
    154                 filter_subject: "import.sign_request".to_string(),
    155                 ..Default::default()
    156             })
    157             .await?;
    158 
    159         Ok(Self {
    160             export_folder,
    161             c2pa_folder,
    162             consumer,
    163         })
    164     }
    165 
    166     pub async fn consumer(&self) -> color_eyre::Result<()> {
    167         let mut messages = self.consumer.messages().await?;
    168         info!("Signing consumer started");
    169         let (_, content_credentials_certificate) = load_certificates(self.c2pa_folder.clone())?;
    170         while let Some(msg) = messages.next().await {
    171             match msg {
    172                 Ok(msg) => {
    173                     let event: super::jetstream_events::C2PASignRequest =
    174                         bincode::deserialize(&msg.payload).unwrap();
    175                     debug!("Received message: {:#?}", event);
    176 
    177                     // Initial ack to notify that we received the message
    178                     msg.ack_with(AckKind::Progress).await.unwrap();
    179 
    180                     // Check if the file still is in the export
    181 
    182                     // First get absolute export folder path
    183                     let export_folder = self.export_folder.canonicalize()?;
    184 
    185                     // Check if the file is still there
    186                     if !event.path.exists() {
    187                         error!("[C2PA Signer] file does not exist: {:?}", event.path);
    188                         // We also acknowledge the message to remove it from the queue
    189                         msg.ack_with(AckKind::Term).await.unwrap();
    190                         continue;
    191                     }
    192 
    193                     // Then check if the file is in the import folder
    194                     if !event.path.starts_with(export_folder.clone()) {
    195                         error!(
    196                             "[C2PA Signer] file is not in the export_folder folder: {:?}",
    197                             event.path
    198                         );
    199 
    200                         // We also acknowledge the message to remove it from the queue
    201                         msg.ack_with(AckKind::Term).await.unwrap();
    202                         continue;
    203                     }
    204 
    205                     // Ignore if there is a c2pa file with the same name next to it
    206                     let c2pa_path = event.path.with_extension("c2pa");
    207                     if c2pa_path.exists() {
    208                         error!("[C2PA Signer] c2pa file already exists: {:?}", c2pa_path);
    209                         // We also acknowledge the message to remove it from the queue
    210                         msg.ack_with(AckKind::Term).await.unwrap();
    211                         continue;
    212                     }
    213 
    214                     // Another progress ack to notify that we are processing the file
    215                     msg.ack_with(AckKind::Progress).await.unwrap();
    216 
    217                     let image_path = &event.path;
    218                     let cloned_path = image_path.clone();
    219                     let filename = cloned_path.file_name().unwrap().to_str().unwrap();
    220 
    221                     let exif_data = event.exif.map(|exif| exif.into());
    222 
    223                     if let Ok(reader) = Reader::from_file(image_path) {
    224                         if let Some(manifest) = reader.active_manifest() {
    225                             if manifest.claim_generator.contains(APP_NAME) {
    226                                 error!("[C2PA Signer] file already signed by us: {:?}", event.path);
    227                                 // We also acknowledge the message to remove it from the queue
    228                                 msg.ack_with(AckKind::Term).await.unwrap();
    229                                 continue;
    230                             }
    231                         }
    232                     }
    233 
    234                     sign_image(
    235                         image_path.clone(),
    236                         FileData::new(Some(image_path.clone()), None, Some(filename.to_string())),
    237                         exif_data,
    238                         content_credentials_certificate.clone(),
    239                     )?;
    240 
    241                     // Acknowledge the message to remove it from the queue
    242                     msg.ack_with(AckKind::Ack).await.unwrap();
    243                     tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    244                 }
    245                 Err(e) => {
    246                     error!("[C2PA Signer] error receiving message: {:?}", e);
    247                 }
    248             }
    249         }
    250         error!("[C2PA Signer] Consumer stopped");
    251         Ok(())
    252     }
    253 }