Skip to main content

tuwunel_service/media/
migrations.rs

1use 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
20/// Migrates a media directory from legacy base64 file names to sha2 file names.
21/// All errors are fatal. Upon success the database is keyed to not perform this
22/// again.
23pub(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	// Move old media files to new names
31	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	// move the file to the new location
44	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
58/// Check is run on startup for prior-migrated media directories. This handles:
59/// - Going back and forth to non-sha256 legacy binaries (e.g. upstream).
60/// - Deletion of artifacts in the media directory which will then fall out of
61///   sync with the database.
62pub(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}