Skip to main content

tuwunel_service/media/
migrations.rs

1use 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
28/// Migrates a media directory from legacy base64 file names to sha2 file names.
29/// All errors are fatal. Upon success the database is keyed to not perform this
30/// again.
31pub(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	// Move old media files to new names
39	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	// move the file to the new location
52	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
66/// Check is run on startup for prior-migrated media directories. This handles:
67/// - Going back and forth to non-sha256 legacy binaries (e.g. upstream).
68/// - Deletion of artifacts in the media directory which will then fall out of
69///   sync with the database.
70pub(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
183/// Links `link` to the file at `target`.
184///
185/// `tokio::fs::symlink` is unix-only. Windows distinguishes a link to a file
186/// from a link to a directory and offers `symlink_file` for the former, which
187/// is what every caller here wants. Elsewhere there is no portable equivalent
188/// to call.
189async 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}