// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::borrow::Cow; use std::path::Path; use serde::Deserialize; use serde_tuple::Serialize_tuple; use tracing::debug; use crate::media::files::data_for_file; use crate::media::files::normalize_filename; use crate::prelude::*; use crate::sync::media::database::client::MediaDatabase; use crate::sync::media::database::client::MediaEntry; use crate::sync::media::MAX_INDIVIDUAL_MEDIA_FILE_SIZE; use crate::sync::media::MEDIA_SYNC_TARGET_ZIP_BYTES; #[derive(Serialize_tuple, Deserialize, Debug)] pub struct MediaUploadResponse { /// Always equal to number of uploaded files now. Old AnkiWeb versions used /// to terminate processing early if too much time had elapsed, so older /// clients will upload the same material again if this is less than the /// count they uploaded. pub processed: usize, pub current_usn: Usn, } /// Filename -> Some(Data), or None in the deleted case. type ZipDataForUpload = Vec<(String, Option>)>; /// Gather [(filename, data)] for provided entries, up to configured limit. /// Data is None if file is deleted. /// Returns None if one or more of the entries were inaccessible or in the wrong /// format. pub fn gather_zip_data_for_upload( ctx: &MediaDatabase, media_folder: &Path, files: &[MediaEntry], ) -> Result> { let mut invalid_entries = vec![]; let mut accumulated_size = 0; let mut entries = vec![]; for file in files { if accumulated_size > MEDIA_SYNC_TARGET_ZIP_BYTES { break; } #[cfg(target_vendor = "apple")] { use unicode_normalization::is_nfc; if !is_nfc(&file.fname) { // older Anki versions stored non-normalized filenames in the DB; clean them up debug!(fname = file.fname, "clean up non-nfc entry"); invalid_entries.push(&file.fname); continue; } } let file_data = if file.sha1.is_some() { match data_for_file(media_folder, &file.fname) { Ok(data) => data, Err(e) => { debug!("error accessing {}: {}", &file.fname, e); invalid_entries.push(&file.fname); continue; } } } else { // uploading deletion None }; if let Some(data) = file_data { let normalized = normalize_filename(&file.fname); if let Cow::Owned(o) = normalized { debug!("media check required: {} should be {}", &file.fname, o); invalid_entries.push(&file.fname); continue; } if data.is_empty() { invalid_entries.push(&file.fname); continue; } if data.len() > MAX_INDIVIDUAL_MEDIA_FILE_SIZE { invalid_entries.push(&file.fname); continue; } accumulated_size += data.len(); entries.push((file.fname.clone(), Some(data))); debug!(file.fname, kind = "addition", "will upload"); } else { entries.push((file.fname.clone(), None)); debug!(file.fname, kind = "removal", "will upload"); } } if !invalid_entries.is_empty() { // clean up invalid entries; we'll build a new zip ctx.transact(|ctx| { for fname in invalid_entries { ctx.remove_entry(fname)?; } Ok(()) })?; return Ok(None); } Ok(Some(entries)) }