tuwunel_service/media/
migrations.rs1use std::{
2 collections::HashSet,
3 ffi::{OsStr, OsString},
4 fs, io,
5 path::{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};
17use tuwunel_database::{Database, Map};
18
19use crate::Services;
20
21struct MediaStorage<'a> {
22 database: &'a Arc<Database>,
23 mediaid_file: &'a Arc<Map>,
24 mediaid_user: &'a Arc<Map>,
25}
26
27pub(crate) async fn migrate_sha256_media(services: &Services) -> Result {
31 let db = &services.db;
32 let config = &services.server.config;
33
34 warn!("Migrating legacy base64 file names to sha256 file names");
35 let mediaid_file = &db["mediaid_file"];
36
37 let mut changes = Vec::<(PathBuf, PathBuf)>::new();
39 mediaid_file
40 .raw_keys()
41 .ignore_err()
42 .ready_for_each(|key| {
43 let old = services.media.get_media_path_b64(key);
44 let new = services.media.get_media_path_sha256(key);
45 debug!(?key, ?old, ?new, num = changes.len(), "change");
46 changes.push((old, new));
47 })
48 .await;
49
50 for (old_path, path) in changes {
52 if old_path.exists() {
53 tokio::fs::rename(&old_path, &path).await?;
54 if config.media_compat_file_link {
55 symlink_file(&path, &old_path).await?;
56 }
57 }
58 }
59
60 db["global"].insert(b"feat_sha256_media", []);
61 info!("Finished applying sha256_media");
62 Ok(())
63}
64
65pub(crate) async fn checkup_sha256_media(services: &Services) -> Result {
70 use crate::media::encode_key;
71
72 debug!("Checking integrity of media directory");
73 let db = &services.db;
74 let media = &services.media;
75 let config = &services.server.config;
76 let mediaid_file = &db["mediaid_file"];
77 let mediaid_user = &db["mediaid_user"];
78 let storage = MediaStorage { database: db, mediaid_file, mediaid_user };
79 let timer = Instant::now();
80
81 let dir = media.get_media_dir();
82 let files: HashSet<OsString> = fs::read_dir(dir)
83 .inspect_err(inspect_debug_log)
84 .into_iter()
85 .flatten()
86 .filter_map(|ent| ent.map_or(None, |ent| Some(ent.path().into_os_string())))
87 .collect();
88
89 for key in media.db.get_all_media_keys().await {
90 let new_path = media.get_media_path_sha256(&key).into_os_string();
91 let old_path = media.get_media_path_b64(&key).into_os_string();
92 if let Err(e) =
93 handle_media_check(&storage, config, &files, &key, &new_path, &old_path).await
94 {
95 error!(
96 media_id = ?encode_key(&key), ?new_path, ?old_path,
97 "Failed to resolve media check failure: {e}"
98 );
99 }
100 }
101
102 debug_info!(
103 elapsed = ?timer.elapsed(),
104 "Finished checking media directory"
105 );
106
107 Ok(())
108}
109
110async fn handle_media_check(
111 storage: &MediaStorage<'_>,
112 config: &Config,
113 files: &HashSet<OsString>,
114 key: &[u8],
115 new_path: &OsStr,
116 old_path: &OsStr,
117) -> Result {
118 use crate::media::encode_key;
119
120 let new_exists = files.contains(new_path);
121 let old_exists = files.contains(old_path);
122 let old_is_symlink = async || {
123 tokio::fs::symlink_metadata(old_path)
124 .await
125 .is_ok_and(|md| md.is_symlink())
126 };
127
128 if config.prune_missing_media && !old_exists && !new_exists {
129 error!(
130 media_id = ?encode_key(key), ?new_path, ?old_path,
131 "Media is missing at all paths. Removing from database..."
132 );
133
134 let mut txn = storage.database.txn();
135
136 txn.del_raw(storage.mediaid_file, key);
137 txn.del_raw(storage.mediaid_user, key);
138 txn.execute();
139 }
140
141 if config.media_compat_file_link && !old_exists && new_exists {
142 debug_warn!(
143 media_id = ?encode_key(key), ?new_path, ?old_path,
144 "Media found but missing legacy link. Fixing..."
145 );
146
147 symlink_file(&new_path, &old_path).await?;
148 }
149
150 if config.media_compat_file_link && !new_exists && old_exists {
151 debug_warn!(
152 media_id = ?encode_key(key), ?new_path, ?old_path,
153 "Legacy media found without sha256 migration. Fixing..."
154 );
155
156 debug_assert!(
157 old_is_symlink().await,
158 "Legacy media not expected to be a symlink without an existing sha256 migration."
159 );
160
161 tokio::fs::rename(&old_path, &new_path).await?;
162 symlink_file(&new_path, &old_path).await?;
163 }
164
165 if !config.media_compat_file_link && old_exists && old_is_symlink().await {
166 debug_warn!(
167 media_id = ?encode_key(key), ?new_path, ?old_path,
168 "Legacy link found but compat disabled. Cleansing symlink..."
169 );
170
171 debug_assert!(
172 new_exists,
173 "sha256 migration into new file expected prior to cleaning legacy symlink here."
174 );
175
176 tokio::fs::remove_file(&old_path).await?;
177 }
178
179 Ok(())
180}
181
182async fn symlink_file(target: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
189 #[cfg(unix)]
190 {
191 tokio::fs::symlink(target, link).await
192 }
193
194 #[cfg(windows)]
195 {
196 tokio::fs::symlink_file(target, link).await
197 }
198
199 #[cfg(not(any(unix, windows)))]
200 {
201 _ = (target, link);
202
203 Err(io::Error::new(
204 io::ErrorKind::Unsupported,
205 "Symlinks are not supported on this platform.",
206 ))
207 }
208}