Skip to main content

tuwunel_service/media/
migrations.rs

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