tuwunel_service/media/
migrations.rs1use std::{
2 collections::HashSet,
3 ffi::{OsStr, OsString},
4 fs::{self},
5 path::PathBuf,
6 sync::Arc,
7 time::Instant,
8};
9
10use tuwunel_core::{
11 Config, Result, debug, debug_info, debug_warn, error,
12 error::inspect_debug_log,
13 info,
14 utils::{ReadyExt, stream::TryIgnore},
15 warn,
16};
17
18use crate::Services;
19
20pub(crate) async fn migrate_sha256_media(services: &Services) -> Result {
24 let db = &services.db;
25 let config = &services.server.config;
26
27 warn!("Migrating legacy base64 file names to sha256 file names");
28 let mediaid_file = &db["mediaid_file"];
29
30 let mut changes = Vec::<(PathBuf, PathBuf)>::new();
32 mediaid_file
33 .raw_keys()
34 .ignore_err()
35 .ready_for_each(|key| {
36 let old = services.media.get_media_path_b64(key);
37 let new = services.media.get_media_path_sha256(key);
38 debug!(?key, ?old, ?new, num = changes.len(), "change");
39 changes.push((old, new));
40 })
41 .await;
42
43 for (old_path, path) in changes {
45 if old_path.exists() {
46 tokio::fs::rename(&old_path, &path).await?;
47 if config.media_compat_file_link {
48 tokio::fs::symlink(&path, &old_path).await?;
49 }
50 }
51 }
52
53 db["global"].insert(b"feat_sha256_media", []);
54 info!("Finished applying sha256_media");
55 Ok(())
56}
57
58pub(crate) async fn checkup_sha256_media(services: &Services) -> Result {
63 use crate::media::encode_key;
64
65 debug!("Checking integrity of media directory");
66 let db = &services.db;
67 let media = &services.media;
68 let config = &services.server.config;
69 let mediaid_file = &db["mediaid_file"];
70 let mediaid_user = &db["mediaid_user"];
71 let dbs = (mediaid_file, mediaid_user);
72 let timer = Instant::now();
73
74 let dir = media.get_media_dir();
75 let files: HashSet<OsString> = fs::read_dir(dir)
76 .inspect_err(inspect_debug_log)
77 .into_iter()
78 .flatten()
79 .filter_map(|ent| ent.map_or(None, |ent| Some(ent.path().into_os_string())))
80 .collect();
81
82 for key in media.db.get_all_media_keys().await {
83 let new_path = media.get_media_path_sha256(&key).into_os_string();
84 let old_path = media.get_media_path_b64(&key).into_os_string();
85 if let Err(e) = handle_media_check(&dbs, config, &files, &key, &new_path, &old_path).await
86 {
87 error!(
88 media_id = ?encode_key(&key), ?new_path, ?old_path,
89 "Failed to resolve media check failure: {e}"
90 );
91 }
92 }
93
94 debug_info!(
95 elapsed = ?timer.elapsed(),
96 "Finished checking media directory"
97 );
98
99 Ok(())
100}
101
102async fn handle_media_check(
103 dbs: &(&Arc<tuwunel_database::Map>, &Arc<tuwunel_database::Map>),
104 config: &Config,
105 files: &HashSet<OsString>,
106 key: &[u8],
107 new_path: &OsStr,
108 old_path: &OsStr,
109) -> Result {
110 use crate::media::encode_key;
111
112 let (mediaid_file, mediaid_user) = dbs;
113
114 let new_exists = files.contains(new_path);
115 let old_exists = files.contains(old_path);
116 let old_is_symlink = || async {
117 tokio::fs::symlink_metadata(old_path)
118 .await
119 .is_ok_and(|md| md.is_symlink())
120 };
121
122 if config.prune_missing_media && !old_exists && !new_exists {
123 error!(
124 media_id = ?encode_key(key), ?new_path, ?old_path,
125 "Media is missing at all paths. Removing from database..."
126 );
127
128 mediaid_file.remove(key);
129 mediaid_user.remove(key);
130 }
131
132 if config.media_compat_file_link && !old_exists && new_exists {
133 debug_warn!(
134 media_id = ?encode_key(key), ?new_path, ?old_path,
135 "Media found but missing legacy link. Fixing..."
136 );
137
138 tokio::fs::symlink(&new_path, &old_path).await?;
139 }
140
141 if config.media_compat_file_link && !new_exists && old_exists {
142 debug_warn!(
143 media_id = ?encode_key(key), ?new_path, ?old_path,
144 "Legacy media found without sha256 migration. Fixing..."
145 );
146
147 debug_assert!(
148 old_is_symlink().await,
149 "Legacy media not expected to be a symlink without an existing sha256 migration."
150 );
151
152 tokio::fs::rename(&old_path, &new_path).await?;
153 tokio::fs::symlink(&new_path, &old_path).await?;
154 }
155
156 if !config.media_compat_file_link && old_exists && old_is_symlink().await {
157 debug_warn!(
158 media_id = ?encode_key(key), ?new_path, ?old_path,
159 "Legacy link found but compat disabled. Cleansing symlink..."
160 );
161
162 debug_assert!(
163 new_exists,
164 "sha256 migration into new file expected prior to cleaning legacy symlink here."
165 );
166
167 tokio::fs::remove_file(&old_path).await?;
168 }
169
170 Ok(())
171}