1use crate::api_client::ImmichApiClient;
10use crate::app_context::AppContext;
11use crate::config::FolderSyncMethod;
12use crate::config::StartupCatchupMode;
13use crate::config::WatchPathEntry;
14use crate::library::album_sync;
15use crate::monitor::{compute_sha1_chunked, is_supported_media_path, is_temporary_file};
16use crate::queue_manager::{FileTask, QueueManager};
17use crate::state_manager::AppState;
18use crate::sync_index::{ShardedSyncIndex, SyncDecision, SyncTarget};
19use futures_util::stream::{self, StreamExt};
20use parking_lot::Mutex;
21use rayon::prelude::*;
22use std::collections::{HashMap, HashSet};
23use std::path::Path;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicUsize, Ordering};
26
27struct ScanCandidate {
29 path: String,
31 watch_path: String,
33 album_name: String,
35 sidecar_enabled: bool,
37}
38
39pub async fn queue_unsynced_files(
44 watch_paths: Vec<WatchPathEntry>,
45 queue_manager: Arc<QueueManager>,
46 sync_index: Arc<ShardedSyncIndex>,
47 api_client: Arc<ImmichApiClient>,
48 catchup_mode: StartupCatchupMode,
49 shared_state: Arc<Mutex<AppState>>,
50 app_ctx: Arc<AppContext>,
51) {
52 if watch_paths.is_empty() {
53 return;
54 }
55
56 let last_sync = shared_state
61 .lock()
62 .last_successful_sync_at
63 .unwrap_or_default();
64
65 let global_xmp_enabled = app_ctx.config.read().data.upload_xmp_sidecars;
66
67 let (candidates, mut seen_paths, skipped_enum, enum_errors) = enumerate_candidates(
68 &watch_paths,
69 catchup_mode,
70 last_sync,
71 &shared_state,
72 global_xmp_enabled,
73 );
74
75 let album_id_cache = resolve_album_ids_for_candidates(&candidates, &api_client).await;
78
79 let prepared: Arc<Mutex<Vec<FileTask>>> = Arc::new(Mutex::new(Vec::new()));
83 let skipped = Arc::new(AtomicUsize::new(skipped_enum));
84 let errors = Arc::new(AtomicUsize::new(enum_errors));
85
86 stream::iter(candidates)
87 .for_each_concurrent(16, |candidate| {
88 let sync_index = sync_index.clone();
89 let api_client = api_client.clone();
90 let album_cache = album_id_cache.clone();
91 let prepared = prepared.clone();
92 let skipped = skipped.clone();
93 let errors = errors.clone();
94
95 async move {
96 let path = Path::new(&candidate.path);
97 let album_name = candidate.album_name.clone();
98
99 let existing_album_id = album_cache.lock().get(&album_name).cloned().flatten();
101
102 let target = SyncTarget {
103 album_name: Some(album_name.clone()),
104 album_id: existing_album_id,
105 };
106
107 let decision = match sync_index.sync_decision(path, &target) {
109 Ok(d) => d,
110 Err(err) => {
111 errors.fetch_add(1, Ordering::Relaxed);
112 log::warn!(
113 "Startup scan could not inspect '{}': {}",
114 candidate.path,
115 err
116 );
117 return;
118 }
119 };
120
121 let (reassociate_only, cached_checksum) = match decision {
122 SyncDecision::UpToDate => {
123 skipped.fetch_add(1, Ordering::Relaxed);
124 return;
125 }
126 SyncDecision::NeedsUpload => (false, None),
127 SyncDecision::NeedsReassociate => {
128 (true, sync_index.stored_checksum(&candidate.path))
129 }
130 };
131
132 let album_id = match resolve_album(&api_client, &album_name, &album_cache).await {
133 Ok(id) => id,
134 Err(err) => {
135 errors.fetch_add(1, Ordering::Relaxed);
136 log::warn!(
137 "Startup scan skipping '{}': album resolution failed: {}",
138 candidate.path,
139 err
140 );
141 return;
142 }
143 };
144
145 match hash_to_task(
146 candidate.path,
147 candidate.watch_path,
148 album_id,
149 Some(album_name),
150 reassociate_only,
151 cached_checksum,
152 candidate.sidecar_enabled,
153 )
154 .await
155 {
156 Ok(task) => prepared.lock().push(task),
157 Err(()) => {
158 errors.fetch_add(1, Ordering::Relaxed);
159 }
160 }
161 }
162 })
163 .await;
164
165 let prepared_tasks: Vec<FileTask> = std::mem::take(&mut *prepared.lock());
167 let (to_upload, reassociated_count) =
168 filter_and_reassociate_existing(prepared_tasks, &api_client, &sync_index).await;
169
170 let reassociated = Arc::new(AtomicUsize::new(reassociated_count));
171
172 let queued = Arc::new(AtomicUsize::new(0));
173 for task in to_upload {
174 if queue_manager.add_to_queue(task).await {
175 queued.fetch_add(1, Ordering::Relaxed);
176 }
177 }
178
179 trash_remote_assets_for_missing_local_files(
180 &watch_paths,
181 &seen_paths,
182 sync_index.clone(),
183 api_client.clone(),
184 )
185 .await;
186
187 sync_album_to_folder_entries(&watch_paths, app_ctx).await;
188
189 prune_index_entries_for_missing_files(&watch_paths, &mut seen_paths, &sync_index);
190
191 let total_queued = queued.load(Ordering::Relaxed);
192 let total_skipped = skipped.load(Ordering::Relaxed);
193 let total_errors = errors.load(Ordering::Relaxed);
194 let total_reassociated = reassociated.load(Ordering::Relaxed);
195
196 if total_queued == 0 && total_reassociated == 0 {
197 log::info!(
198 "Startup scan complete: no unsynced files found ({} already current, {} error(s)).",
199 total_skipped,
200 total_errors
201 );
202 return;
203 }
204
205 log::info!(
206 "Startup scan: queued={} reassociated={} skipped={} errors={}",
207 total_queued,
208 total_reassociated,
209 total_skipped,
210 total_errors
211 );
212}
213
214async fn trash_remote_assets_for_missing_local_files(
216 watch_paths: &[WatchPathEntry],
217 seen_paths: &HashSet<String>,
218 sync_index: Arc<ShardedSyncIndex>,
219 api_client: Arc<ImmichApiClient>,
220) {
221 for entry in watch_paths {
222 let rules = entry.rules();
223 if !rules.delete_folder_to_album {
224 continue;
225 }
226
227 let root = Path::new(entry.path());
228 for (path, record) in sync_index.records_under_path(root) {
229 if seen_paths.contains(&path) {
230 continue;
231 }
232
233 let album_name = entry
234 .album_name()
235 .map(|name| name.to_string())
236 .or(record.album_name.clone())
237 .or_else(|| {
238 Path::new(&path)
239 .parent()
240 .and_then(|parent| parent.file_name())
241 .map(|name| name.to_string_lossy().to_string())
242 })
243 .unwrap_or_else(|| "Mimick".to_string());
244
245 let configured_album_id = match entry {
246 WatchPathEntry::WithConfig { album_id, .. } => album_id.clone(),
247 WatchPathEntry::Simple(_) => None,
248 };
249 let Some(album_id) = configured_album_id.or(record.album_id.clone()) else {
250 match api_client.get_album_id_if_exists(&album_name).await {
251 Ok(Some(album_id)) => {
252 if trash_remote_asset_by_checksum(
253 &api_client,
254 &sync_index,
255 &path,
256 &album_id,
257 &record.checksum,
258 &album_name,
259 )
260 .await
261 {
262 continue;
263 }
264 }
265 Ok(None) => {}
266 Err(err) => {
267 log::warn!(
268 "Startup deletion sync could not resolve album '{}': {}",
269 album_name,
270 err
271 );
272 }
273 }
274 continue;
275 };
276
277 trash_remote_asset_by_checksum(
278 &api_client,
279 &sync_index,
280 &path,
281 &album_id,
282 &record.checksum,
283 &album_name,
284 )
285 .await;
286 }
287 }
288}
289
290async fn sync_album_to_folder_entries(watch_paths: &[WatchPathEntry], app_ctx: Arc<AppContext>) {
292 for entry in watch_paths {
293 reconcile_entry(app_ctx.clone(), entry).await;
294 }
295}
296
297pub async fn reconcile_entry(app_ctx: Arc<AppContext>, entry: &WatchPathEntry) {
301 let rules = entry.rules();
302 let download_enabled = rules.sync_method != FolderSyncMethod::UploadOnly;
303 if !download_enabled && !rules.delete_album_to_folder && !rules.delete_folder_to_album {
304 return;
305 }
306
307 let watch_path = Path::new(entry.path()).to_path_buf();
308 if !watch_path.is_dir() {
309 return;
310 }
311
312 let album_name = entry
313 .album_name()
314 .map(|name| name.to_string())
315 .or_else(|| {
316 watch_path
317 .file_name()
318 .map(|name| name.to_string_lossy().to_string())
319 })
320 .unwrap_or_else(|| "Mimick".to_string());
321
322 let configured_album_id = match entry {
323 WatchPathEntry::WithConfig { album_id, .. } => album_id.clone(),
324 WatchPathEntry::Simple(_) => None,
325 };
326 let album_id = match configured_album_id {
327 Some(id) => Some(id),
328 None => match app_ctx.api_client.get_album_id_if_exists(&album_name).await {
329 Ok(id) => id,
330 Err(err) => {
331 log::warn!(
332 "Album-to-folder sync could not resolve album '{}': {}",
333 album_name,
334 err
335 );
336 None
337 }
338 },
339 };
340 let Some(album_id) = album_id else {
341 return;
342 };
343
344 let _guard = match app_ctx.reconcile_locks.try_acquire(album_id.clone()) {
347 Some(g) => g,
348 None => {
349 log::debug!(
350 "Reconcile already in progress for album {}, skipping",
351 album_id
352 );
353 return;
354 }
355 };
356
357 let diff = match album_sync::diff_album_vs_folder(
358 app_ctx.clone(),
359 &album_id,
360 &watch_path,
361 &rules,
362 false,
363 )
364 .await
365 {
366 Ok(diff) => diff,
367 Err(err) => {
368 log::warn!(
369 "Album-to-folder sync diff failed for '{}': {}",
370 album_name,
371 err
372 );
373 return;
374 }
375 };
376
377 execute_reconcile_diff(app_ctx.clone(), &album_name, &album_id, watch_path, diff).await;
378}
379
380async fn execute_reconcile_diff(
381 app_ctx: Arc<AppContext>,
382 album_name: &str,
383 album_id: &str,
384 watch_path: std::path::PathBuf,
385 diff: crate::library::album_sync::AlbumDiff,
386) {
387 if !diff.to_download.is_empty() {
388 let (downloaded, failed) = album_sync::execute_downloads(
389 app_ctx.clone(),
390 watch_path.clone(),
391 Some(album_id.to_string()),
392 Some(album_name.to_string()),
393 diff.to_download,
394 )
395 .await;
396 log::info!(
397 "Album-to-folder sync for '{}' downloaded {} item(s), {} failure(s)",
398 album_name,
399 downloaded,
400 failed
401 );
402 }
403
404 if !diff.to_delete_local.is_empty() {
405 let (trashed, failed) =
406 album_sync::execute_local_deletions(app_ctx.clone(), diff.to_delete_local).await;
407 log::info!(
408 "Album-to-folder deletion sync for '{}' moved {} local item(s) to trash, {} failure(s)",
409 album_name,
410 trashed,
411 failed
412 );
413 }
414
415 if !diff.to_delete_remote.is_empty() {
416 let count = diff.to_delete_remote.len();
417 let trashed =
418 album_sync::execute_remote_deletions(app_ctx.clone(), album_id, diff.to_delete_remote)
419 .await;
420 log::info!(
421 "Folder-to-album deletion sync for '{}' moved {} of {} remote item(s) to Immich trash",
422 album_name,
423 trashed,
424 count
425 );
426 }
427}
428
429fn prune_index_entries_for_missing_files(
430 watch_paths: &[WatchPathEntry],
431 seen_paths: &mut HashSet<String>,
432 sync_index: &ShardedSyncIndex,
433) {
434 for entry in watch_paths {
438 let rules = entry.rules();
439 if !rules.delete_folder_to_album {
440 continue;
441 }
442 let root = Path::new(entry.path());
443 for (path, _) in sync_index.records_under_path(root) {
444 seen_paths.insert(path);
445 }
446 }
447 if let Err(err) = sync_index.prune_missing(seen_paths) {
448 log::warn!("Failed to prune sync index after startup scan: {}", err);
449 }
450}
451
452async fn trash_remote_asset_by_checksum(
454 api_client: &ImmichApiClient,
455 sync_index: &ShardedSyncIndex,
456 local_path: &str,
457 album_id: &str,
458 checksum: &str,
459 album_name: &str,
460) -> bool {
461 match find_album_asset_id_by_checksum(api_client, album_id, checksum).await {
462 Ok(Some((asset_id, filename))) => {
463 let ids = vec![asset_id];
464 match api_client.delete_assets(&ids).await {
465 Ok(()) => {
466 if let Err(err) = sync_index.remove_path(local_path) {
467 log::warn!(
468 "Startup deletion sync trashed '{}' in album '{}' but could not remove sync record for '{}': {}",
469 filename,
470 album_name,
471 local_path,
472 err
473 );
474 }
475 log::info!(
476 "Startup deletion sync moved '{}' in album '{}' to Immich trash",
477 filename,
478 album_name
479 );
480 true
481 }
482 Err(err) => {
483 log::warn!(
484 "Startup deletion sync could not move '{}' in album '{}' to trash: {}",
485 filename,
486 album_name,
487 err
488 );
489 false
490 }
491 }
492 }
493 Ok(None) => false,
494 Err(err) => {
495 log::warn!(
496 "Startup deletion sync could not inspect album '{}': {}",
497 album_name,
498 err
499 );
500 false
501 }
502 }
503}
504
505async fn find_album_asset_id_by_checksum(
507 api_client: &ImmichApiClient,
508 album_id: &str,
509 checksum: &str,
510) -> Result<Option<(String, String)>, String> {
511 let mut page = 1;
512 loop {
513 let (assets, has_more) = api_client
514 .fetch_album_assets(album_id, page, 1000, None)
515 .await?;
516 if let Some(asset) = assets
517 .into_iter()
518 .find(|asset| asset.checksum.as_deref() == Some(checksum))
519 {
520 return Ok(Some((asset.id, asset.filename)));
521 }
522 if !has_more {
523 return Ok(None);
524 }
525 page += 1;
526 }
527}
528
529fn enumerate_candidates(
533 watch_paths: &[WatchPathEntry],
534 fallback_catchup_mode: StartupCatchupMode,
535 last_sync: f64,
536 shared_state: &Arc<Mutex<AppState>>,
537 global_xmp_enabled: bool,
538) -> (Vec<ScanCandidate>, HashSet<String>, usize, usize) {
539 let skipped = AtomicUsize::new(0);
540 let errors = AtomicUsize::new(0);
541 let seen_paths = Mutex::new(HashSet::new());
542
543 let candidates: Vec<ScanCandidate> = watch_paths
544 .par_iter()
545 .flat_map(|entry| {
546 if entry.sync_method() == FolderSyncMethod::DownloadOnly {
547 return Vec::new();
548 }
549
550 let catchup_mode = entry.startup_catchup_mode(&fallback_catchup_mode);
551 let watch_path_str = entry.path().to_string();
552 let root = Path::new(&watch_path_str);
553 if !root.exists() {
554 log::warn!(
555 "Startup scan skipped missing watch path: {}",
556 root.display()
557 );
558 if let Some(mut state) = shared_state.try_lock() {
559 let status = state.folder_statuses.entry(watch_path_str).or_default();
560 status.last_error = Some("Permission lost or folder missing".to_string());
561 }
562 return Vec::new();
563 }
564
565 let mut results = Vec::new();
566 let mut stack = vec![root.to_path_buf()];
567 while let Some(dir) = stack.pop() {
568 let read_dir = match std::fs::read_dir(&dir) {
569 Ok(iter) => iter,
570 Err(err) => {
571 errors.fetch_add(1, Ordering::Relaxed);
572 log::warn!("Startup scan could not read '{}': {}", dir.display(), err);
573 continue;
574 }
575 };
576
577 for child in read_dir {
578 let entry_fs = match child {
579 Ok(e) => e,
580 Err(err) => {
581 errors.fetch_add(1, Ordering::Relaxed);
582 log::warn!("Startup scan directory entry error: {}", err);
583 continue;
584 }
585 };
586
587 let path = entry_fs.path();
588 if path.is_dir() {
589 stack.push(path);
590 continue;
591 }
592
593 if !is_supported_media_path(&path)
594 || is_temporary_file(&path)
595 || !entry.rules().matches(&path)
596 {
597 continue;
598 }
599
600 if should_skip_for_catchup(&catchup_mode, &entry_fs, last_sync) {
601 skipped.fetch_add(1, Ordering::Relaxed);
602 continue;
603 }
604
605 let path_str = path.to_string_lossy().into_owned();
606 seen_paths.lock().insert(path_str.clone());
607 let album_name = effective_album_name(entry, &path);
608 let sidecar_enabled = entry.rules().xmp_sidecar_enabled(global_xmp_enabled);
609 results.push(ScanCandidate {
610 path: path_str,
611 watch_path: watch_path_str.clone(),
612 album_name,
613 sidecar_enabled,
614 });
615 }
616 }
617 results
618 })
619 .collect();
620
621 (
622 candidates,
623 seen_paths.into_inner(),
624 skipped.into_inner(),
625 errors.into_inner(),
626 )
627}
628
629fn should_skip_for_catchup(
630 mode: &StartupCatchupMode,
631 entry_fs: &std::fs::DirEntry,
632 last_sync: f64,
633) -> bool {
634 let meta = match entry_fs.metadata() {
635 Ok(m) => m,
636 Err(_) => return false,
637 };
638
639 match mode {
640 StartupCatchupMode::RecentOnly => {
641 let Ok(modified) = meta.modified() else {
642 return false;
643 };
644 let Ok(duration) = std::time::SystemTime::now().duration_since(modified) else {
645 return false;
646 };
647 duration.as_secs() > 7 * 86400
648 }
649 StartupCatchupMode::NewFilesOnly => {
650 let Ok(created) = meta.created().or_else(|_| meta.modified()) else {
651 return false;
652 };
653 let created_secs = created
654 .duration_since(std::time::UNIX_EPOCH)
655 .unwrap_or_default()
656 .as_secs_f64();
657 created_secs < last_sync
658 }
659 _ => false,
660 }
661}
662
663async fn hash_to_task(
665 path: String,
666 watch_path: String,
667 album_id: Option<String>,
668 album_name: Option<String>,
669 reassociate_only: bool,
670 checksum: Option<String>,
671 sidecar_enabled: bool,
672) -> Result<FileTask, ()> {
673 let checksum = if let Some(checksum) = checksum {
674 checksum
675 } else {
676 let path_for_hash = path.clone();
677 match tokio::task::spawn_blocking(move || compute_sha1_chunked(&path_for_hash)).await {
678 Ok(Ok(checksum)) => checksum,
679 Ok(Err(err)) => {
680 log::warn!("Startup scan could not checksum '{}': {}", path, err);
681 return Err(());
682 }
683 Err(err) => {
684 log::warn!("Startup scan checksum task failed for '{}': {}", path, err);
685 return Err(());
686 }
687 }
688 };
689
690 let sidecar_path = if sidecar_enabled {
691 crate::sidecar::find_sidecar(std::path::Path::new(&path))
692 .map(|p| p.to_string_lossy().into_owned())
693 } else {
694 None
695 };
696
697 Ok(FileTask {
698 path,
699 watch_path,
700 checksum,
701 album_id,
702 album_name,
703 reassociate_only,
704 skip_album: false,
705 sidecar_path,
706 })
707}
708
709fn effective_album_name(entry: &WatchPathEntry, path: &Path) -> String {
711 match entry.album_name() {
712 Some(name) if !name.is_empty() && name != "Default (Folder Name)" => name.to_string(),
713 _ => path
714 .parent()
715 .and_then(|p| p.file_name())
716 .map(|n| n.to_string_lossy().to_string())
717 .unwrap_or_else(|| "Mimick".to_string()),
718 }
719}
720
721async fn resolve_album(
724 api_client: &ImmichApiClient,
725 album_name: &str,
726 cache: &Arc<Mutex<HashMap<String, Option<String>>>>,
727) -> Result<Option<String>, String> {
728 if let Some(Some(cached)) = cache.lock().get(album_name) {
730 return Ok(Some(cached.clone()));
731 }
732
733 let resolved = api_client.resolve_album_by_name(album_name, false).await?;
734 cache
735 .lock()
736 .insert(album_name.to_string(), resolved.clone());
737 Ok(resolved)
738}
739
740async fn resolve_album_ids_for_candidates(
741 candidates: &[ScanCandidate],
742 api_client: &Arc<ImmichApiClient>,
743) -> Arc<Mutex<HashMap<String, Option<String>>>> {
744 let unique_albums: Vec<String> = candidates
745 .iter()
746 .map(|c| c.album_name.clone())
747 .collect::<HashSet<_>>()
748 .into_iter()
749 .collect();
750
751 let album_id_cache: Arc<Mutex<HashMap<String, Option<String>>>> =
752 Arc::new(Mutex::new(HashMap::new()));
753
754 let cache = album_id_cache.clone();
755 stream::iter(unique_albums)
756 .for_each_concurrent(8, |name| {
757 let api = api_client.clone();
758 let cache = cache.clone();
759 async move {
760 match api.get_album_id_if_exists(&name).await {
761 Ok(id) => {
762 cache.lock().insert(name, id);
763 }
764 Err(err) => {
765 log::warn!("Startup scan: album lookup failed for '{}': {}", name, err);
766 }
767 }
768 }
769 })
770 .await;
771
772 album_id_cache
773}
774
775async fn filter_and_reassociate_existing(
776 prepared_tasks: Vec<FileTask>,
777 api_client: &Arc<ImmichApiClient>,
778 sync_index: &Arc<ShardedSyncIndex>,
779) -> (Vec<FileTask>, usize) {
780 let unique_checksums: Vec<String> = prepared_tasks
781 .iter()
782 .map(|t| t.checksum.clone())
783 .collect::<HashSet<_>>()
784 .into_iter()
785 .collect();
786 let existing_on_server = if unique_checksums.is_empty() {
787 HashMap::new()
788 } else {
789 api_client.bulk_existing_asset_ids(&unique_checksums).await
790 };
791
792 let mut to_reassociate: Vec<(FileTask, String)> = Vec::new();
793 let mut to_upload: Vec<FileTask> = Vec::new();
794 for task in prepared_tasks {
795 match existing_on_server.get(&task.checksum) {
796 Some(asset_id) => to_reassociate.push((task, asset_id.clone())),
797 None => to_upload.push(task),
798 }
799 }
800
801 let reassociated = Arc::new(AtomicUsize::new(0));
802 process_reassociation(api_client, sync_index, to_reassociate, reassociated.clone()).await;
803
804 (to_upload, reassociated.load(Ordering::Relaxed))
805}
806
807async fn process_reassociation(
808 api_client: &Arc<ImmichApiClient>,
809 sync_index: &Arc<ShardedSyncIndex>,
810 to_reassociate: Vec<(FileTask, String)>,
811 reassociated: Arc<AtomicUsize>,
812) {
813 stream::iter(to_reassociate)
814 .for_each_concurrent(8, |(task, asset_id)| {
815 let api = api_client.clone();
816 let sync_index = sync_index.clone();
817 let reassociated = reassociated.clone();
818 async move {
819 if let Some(ref album_id) = task.album_id
820 && !album_id.is_empty()
821 {
822 let _ = api
823 .add_assets_to_album(album_id, std::slice::from_ref(&asset_id))
824 .await;
825 }
826 let target = SyncTarget {
827 album_name: task.album_name.clone(),
828 album_id: task.album_id.clone(),
829 };
830 if let Err(err) = sync_index.record_synced(&task.path, &task.checksum, &target) {
831 log::warn!(
832 "Could not record sync index for pre-existing asset '{}': {}",
833 task.path,
834 err
835 );
836 }
837 reassociated.fetch_add(1, Ordering::Relaxed);
838 }
839 })
840 .await;
841}
842
843#[cfg(test)]
844mod tests {
845 use crate::monitor::is_supported_media_path;
846 use std::path::PathBuf;
847
848 #[test]
849 fn test_supported_media_path_filter() {
850 assert!(is_supported_media_path(&PathBuf::from("image.avif")));
851 assert!(is_supported_media_path(&PathBuf::from("image.jpg")));
852 assert!(is_supported_media_path(&PathBuf::from("movie.mkv")));
853 assert!(is_supported_media_path(&PathBuf::from("movie.mp4")));
854 assert!(!is_supported_media_path(&PathBuf::from("notes.txt")));
855 }
856}