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